1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-27 04:45:20 +01:00

Merge branch '5.x' into upstream-master

This commit is contained in:
Bruce Weirdan 2023-08-26 01:57:03 +02:00
commit 190d488196
No known key found for this signature in database
GPG Key ID: CFC3AAB181751B0D
8 changed files with 117 additions and 42 deletions

View File

@ -12987,7 +12987,7 @@ return [
'strpbrk' => ['string|false', 'string'=>'string', 'characters'=>'string'], 'strpbrk' => ['string|false', 'string'=>'string', 'characters'=>'string'],
'strpos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int'], 'strpos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int'],
'strptime' => ['array|false', 'timestamp'=>'string', 'format'=>'string'], 'strptime' => ['array|false', 'timestamp'=>'string', 'format'=>'string'],
'strrchr' => ['string|false', 'haystack'=>'string', 'needle'=>'string'], 'strrchr' => ['string|false', 'haystack'=>'string', 'needle'=>'string', 'before_needle='=>'bool'],
'strrev' => ['string', 'string'=>'string'], 'strrev' => ['string', 'string'=>'string'],
'strripos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int'], 'strripos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int'],
'strrpos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int'], 'strrpos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int'],

View File

@ -113,6 +113,10 @@ return [
'old' => ['?bool', 'text'=>'string'], 'old' => ['?bool', 'text'=>'string'],
'new' => ['bool', 'text'=>'string'], 'new' => ['bool', 'text'=>'string'],
], ],
'strrchr' => [
'old' => ['string|false', 'haystack'=>'string', 'needle'=>'string'],
'new' => ['string|false', 'haystack'=>'string', 'needle'=>'string', 'before_needle='=>'bool'],
],
], ],
'removed' => [ 'removed' => [

View File

@ -14,7 +14,7 @@ $a = [1, 2, 3, 4, 5];
```php ```php
<?php <?php
$a = [0 => 'hello', 5 => 'goodbye']; $a = [0 => 'hello', 5 => 'goodbye'];
$b = ['a' => 'AA', 'b' => 'BB', 'c' => 'CC'] $b = ['a' => 'AA', 'b' => 'BB', 'c' => 'CC'];
``` ```
Makeshift [Structs](https://en.wikipedia.org/wiki/Struct_(C_programming_language)): Makeshift [Structs](https://en.wikipedia.org/wiki/Struct_(C_programming_language)):

View File

@ -4,7 +4,6 @@ namespace Psalm\Internal\Type\Comparator;
use Psalm\Codebase; use Psalm\Codebase;
use Psalm\Internal\MethodIdentifier; use Psalm\Internal\MethodIdentifier;
use Psalm\Type;
use Psalm\Type\Atomic; use Psalm\Type\Atomic;
use Psalm\Type\Atomic\Scalar; use Psalm\Type\Atomic\Scalar;
use Psalm\Type\Atomic\TArray; use Psalm\Type\Atomic\TArray;
@ -18,11 +17,9 @@ use Psalm\Type\Atomic\TConditional;
use Psalm\Type\Atomic\TEmptyMixed; use Psalm\Type\Atomic\TEmptyMixed;
use Psalm\Type\Atomic\TEnumCase; use Psalm\Type\Atomic\TEnumCase;
use Psalm\Type\Atomic\TGenericObject; use Psalm\Type\Atomic\TGenericObject;
use Psalm\Type\Atomic\TInt;
use Psalm\Type\Atomic\TIterable; use Psalm\Type\Atomic\TIterable;
use Psalm\Type\Atomic\TKeyOf; use Psalm\Type\Atomic\TKeyOf;
use Psalm\Type\Atomic\TKeyedArray; use Psalm\Type\Atomic\TKeyedArray;
use Psalm\Type\Atomic\TLiteralInt;
use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TLiteralString;
use Psalm\Type\Atomic\TMixed; use Psalm\Type\Atomic\TMixed;
use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TNamedObject;
@ -45,7 +42,6 @@ use function array_values;
use function assert; use function assert;
use function count; use function count;
use function get_class; use function get_class;
use function is_int;
use function strtolower; use function strtolower;
/** /**
@ -630,40 +626,6 @@ class AtomicTypeComparator
} }
} }
if ($input_type_part instanceof TEnumCase
&& $codebase->classlike_storage_provider->has($input_type_part->value)
) {
if ($container_type_part instanceof TString || $container_type_part instanceof TInt) {
$input_type_classlike_storage = $codebase->classlike_storage_provider->get($input_type_part->value);
if ($input_type_classlike_storage->enum_type === null
|| !isset($input_type_classlike_storage->enum_cases[$input_type_part->case_name])
) {
// Not a backed enum or non-existent enum case
return false;
}
$input_type_enum_case_storage = $input_type_classlike_storage->enum_cases[$input_type_part->case_name];
assert(
$input_type_enum_case_storage->value !== null,
'Backed enums cannot have values without a value.',
);
if (is_int($input_type_enum_case_storage->value)) {
return self::isContainedBy(
$codebase,
new TLiteralInt($input_type_enum_case_storage->value),
$container_type_part,
);
}
return self::isContainedBy(
$codebase,
Type::getAtomicStringFromLiteral($input_type_enum_case_storage->value),
$container_type_part,
);
}
}
if ($container_type_part instanceof TString || $container_type_part instanceof TScalar) { if ($container_type_part instanceof TString || $container_type_part instanceof TScalar) {
if ($input_type_part instanceof TNamedObject) { if ($input_type_part instanceof TNamedObject) {
// check whether the object has a __toString method // check whether the object has a __toString method

View File

@ -82,6 +82,7 @@ use function assert;
use function count; use function count;
use function explode; use function explode;
use function get_class; use function get_class;
use function in_array;
use function is_int; use function is_int;
use function min; use function min;
use function strlen; use function strlen;
@ -530,7 +531,11 @@ class SimpleAssertionReconciler extends Reconciler
} }
if ($assertion_type instanceof TValueOf) { if ($assertion_type instanceof TValueOf) {
return $assertion_type->type; return self::reconcileValueOf(
$codebase,
$assertion_type,
$failed_reconciliation,
);
} }
return null; return null;
@ -2931,6 +2936,71 @@ class SimpleAssertionReconciler extends Reconciler
return TypeCombiner::combine(array_values($matched_class_constant_types), $codebase); return TypeCombiner::combine(array_values($matched_class_constant_types), $codebase);
} }
/**
* @param Reconciler::RECONCILIATION_* $failed_reconciliation
*/
private static function reconcileValueOf(
Codebase $codebase,
TValueOf $assertion_type,
int &$failed_reconciliation
): ?Union {
$reconciled_types = [];
// For now, only enums are supported here
foreach ($assertion_type->type->getAtomicTypes() as $atomic_type) {
$enum_case_to_assert = null;
if ($atomic_type instanceof TClassConstant) {
$class_name = $atomic_type->fq_classlike_name;
$enum_case_to_assert = $atomic_type->const_name;
} elseif ($atomic_type instanceof TNamedObject) {
$class_name = $atomic_type->value;
} else {
return null;
}
if (!$codebase->classOrInterfaceOrEnumExists($class_name)) {
return null;
}
$class_storage = $codebase->classlike_storage_provider->get($class_name);
if (!$class_storage->is_enum) {
return null;
}
if (!in_array($class_storage->enum_type, ['string', 'int'], true)) {
return null;
}
// For value-of<MyBackedEnum>, the assertion is meant to return *ANY* value of *ANY* enum case
if ($enum_case_to_assert === null) {
foreach ($class_storage->enum_cases as $enum_case) {
assert(
$enum_case->value !== null,
'Verified enum type above, value can not contain `null` anymore.',
);
$reconciled_types[] = Type::getLiteral($enum_case->value);
}
continue;
}
$enum_case = $class_storage->enum_cases[$atomic_type->const_name] ?? null;
if ($enum_case === null) {
return null;
}
assert($enum_case->value !== null, 'Verified enum type above, value can not contain `null` anymore.');
$reconciled_types[] = Type::getLiteral($enum_case->value);
}
if ($reconciled_types === []) {
$failed_reconciliation = Reconciler::RECONCILIATION_EMPTY;
return Type::getNever();
}
return TypeCombiner::combine($reconciled_types, $codebase, false, false);
}
/** /**
* @psalm-assert-if-true TCallableObject|TObjectWithProperties|TNamedObject $type * @psalm-assert-if-true TCallableObject|TObjectWithProperties|TNamedObject $type
*/ */

View File

@ -58,6 +58,7 @@ use function array_values;
use function explode; use function explode;
use function get_class; use function get_class;
use function implode; use function implode;
use function is_int;
use function preg_quote; use function preg_quote;
use function preg_replace; use function preg_replace;
use function stripos; use function stripos;
@ -258,6 +259,19 @@ abstract class Type
return new Union([$type]); return new Union([$type]);
} }
/**
* @param int|string $value
* @return TLiteralString|TLiteralInt
*/
public static function getLiteral($value): Atomic
{
if (is_int($value)) {
return new TLiteralInt($value);
}
return TLiteralString::make($value);
}
public static function getString(?string $value = null): Union public static function getString(?string $value = null): Union
{ {
return new Union([$value === null ? new TString() : self::getAtomicStringFromLiteral($value)]); return new Union([$value === null ? new TString() : self::getAtomicStringFromLiteral($value)]);

View File

@ -2194,6 +2194,10 @@ class AssertAnnotationTest extends TestCase
function assertSomeInt(int $foo): void function assertSomeInt(int $foo): void
{} {}
/** @psalm-assert value-of<StringEnum|IntEnum> $foo */
function assertAnyEnumValue(string|int $foo): void
{}
/** @param "foo"|"bar" $foo */ /** @param "foo"|"bar" $foo */
function takesSomeStringFromEnum(string $foo): StringEnum function takesSomeStringFromEnum(string $foo): StringEnum
{ {
@ -2216,8 +2220,14 @@ class AssertAnnotationTest extends TestCase
assertSomeInt($int); assertSomeInt($int);
takesSomeIntFromEnum($int); takesSomeIntFromEnum($int);
/** @var string|int $potentialEnumValue */
$potentialEnumValue = null;
assertAnyEnumValue($potentialEnumValue);
', ',
'assertions' => [], 'assertions' => [
'$potentialEnumValue===' => "'bar'|'baz'|'foo'|1|2|3",
],
'ignored_issues' => [], 'ignored_issues' => [],
'php_version' => '8.1', 'php_version' => '8.1',
], ],

View File

@ -1015,6 +1015,21 @@ class EnumTest extends TestCase
'ignored_issues' => [], 'ignored_issues' => [],
'php_version' => '8.1', 'php_version' => '8.1',
], ],
'backedEnumDoesNotPassNativeType' => [
'code' => '<?php
enum State: string
{
case A = "A";
case B = "B";
case C = "C";
}
function f(string $state): void {}
f(State::A);
',
'error_message' => 'InvalidArgument',
'ignored_issues' => [],
'php_version' => '8.1',
],
]; ];
} }
} }