1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-11 08:49:52 +01:00
psalm/tests/EnumTest.php

692 lines
22 KiB
PHP
Raw Normal View History

<?php
namespace Psalm\Tests;
2021-12-04 21:55:53 +01:00
use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait;
use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait;
class EnumTest extends TestCase
{
2021-12-04 21:55:53 +01:00
use ValidCodeAnalysisTestTrait;
use InvalidCodeAnalysisTestTrait;
/**
Add support for strict arrays, fix type alias intersection, fix array_is_list assertion on non-lists (#8395) * Immutable CodeLocation * Remove excess clones * Remove external clones * Remove leftover clones * Fix final clone issue * Immutable storages * Refactoring * Fixes * Fixes * Fix * Fix * Fixes * Simplify * Fixes * Fix * Fixes * Update * Fix * Cache global types * Fix * Update * Update * Fixes * Fixes * Refactor * Fixes * Fix * Fix * More caching * Fix * Fix * Update * Update * Fix * Fixes * Update * Refactor * Update * Fixes * Break one more test * Fix * FIx * Fix * Fix * Fix * Fix * Improve performance and readability * Equivalent logic * Fixes * Revert * Revert "Revert" This reverts commit f9175100c8452c80559234200663fd4c4f4dd889. * Fix * Fix reference bug * Make default TypeVisitor immutable * Bugfix * Remove clones * Partial refactoring * Refactoring * Fixes * Fix * Fixes * Fixes * cs-fix * Fix final bugs * Add test * Misc fixes * Update * Fixes * Experiment with removing different property * revert "Experiment with removing different property" This reverts commit ac1156e077fc4ea633530d51096d27b6e88bfdf9. * Uniform naming * Uniform naming * Hack hotfix * Clean up $_FILES ref #8621 * Undo hack, try fixing properly * Helper method * Remove redundant call * Partially fix bugs * Cleanup * Change defaults * Fix bug * Fix (?, hope this doesn't break anything else) * cs-fix * Review fixes * Bugfix * Bugfix * Improve logic * Add support for list{} and callable-list{} types, properly implement array_is_list assertions (fixes #8389) * Default to sealed arrays * Fix array_merge bug * Fixes * Fix * Sealed type checks * Properly infer properties-of and get_object_vars on final classes * Fix array_map zipping * Fix tests * Fixes * Fixes * Fix more stuff * Recursively resolve type aliases * Fix typo * Fixes * Fix array_is_list assertion on keyed array * Add BC docs * Fixes * fix * Update * Update * Update * Update * Seal arrays with count assertions * Fix #8528 * Fix * Update * Improve sealed array foreach logic * get_object_vars on template properties * Fix sealed array assertion reconciler logic * Improved reconciler * Add tests * Single source of truth for test types * Fix tests * Fixup tests * Fixup tests * Fixup tests * Update * Fix tests * Fix tests * Final fixes * Fixes * Use list syntax only when needed * Fix tests * Cs-fix * Update docs * Update docs * Update docs * Update docs * Update docs * Document missing types * Update docs * Improve class-string-map docs * Update * Update * I love working on psalm :) * Keep arrays unsealed by default * Fixup tests * Fix syntax mistake * cs-fix * Fix typo * Re-import missing types * Keep strict types only in return types * argc/argv fixes * argc/argv fixes * Fix test * Comment-out valinor code, pinging @romm pls merge https://github.com/CuyZ/Valinor/pull/246 so we can add valinor to the psalm docs :)
2022-11-05 22:34:42 +01:00
*
*/
public function providerValidCodeParse(): iterable
{
return [
'example' => [
'code' => '<?php
interface Colourful {
public function color(): string;
}
enum Suit implements Colourful {
case Hearts;
case Diamonds;
case Clubs;
case Spades;
public function color(): string {
return match($this) {
Suit::Hearts, Suit::Diamonds => "Red",
Suit::Clubs, Suit::Spades => "Black",
};
}
public function shape(): string {
return "Rectangle";
}
}
function paint(Colourful $c): void {}
function deal(Suit $s): void {
if ($s === Suit::Clubs) {
echo $s->color();
}
}
paint(Suit::Clubs);
deal(Suit::Spades);
Suit::Diamonds->shape();',
'assertions' => [],
'ignored_issues' => [],
'php_version' => '8.1'
],
'enumValue' => [
'code' => '<?php
enum Suit: string {
case Hearts = "h";
case Diamonds = "d";
case Clubs = "c";
case Spades = "s";
}
if (Suit::Hearts->value === "h") {}',
'assertions' => [],
'ignored_issues' => [],
'php_version' => '8.1'
],
'enumCases' => [
'code' => '<?php
enum Suit {
case Hearts;
case Diamonds;
case Clubs;
case Spades;
}
foreach (Suit::cases() as $case) {
echo match($case) {
Suit::Hearts, Suit::Diamonds => "Red",
Suit::Clubs, Suit::Spades => "Black",
};
}',
'assertions' => [],
'ignored_issues' => [],
'php_version' => '8.1'
],
'literalExpressionAsCaseValue' => [
'code' => '<?php
enum Mask: int {
case One = 1 << 0;
case Two = 1 << 1;
}
$z = Mask::Two->value;
',
'assertions' => [
// xxx: we should be able to do better when we reference a case explicitly, like above
'$z===' => '1|2',
],
'ignored_issues' => [],
'php_version' => '8.1'
],
'namePropertyFromOutside' => [
'code' => '<?php
enum Status
{
case DRAFT;
case PUBLISHED;
case ARCHIVED;
}
$a = Status::DRAFT->name;
',
'assertions' => [
'$a===' => "'DRAFT'",
],
'ignored_issues' => [],
'php_version' => '8.1'
],
'namePropertyFromInside' => [
'code' => '<?php
enum Status
{
case DRAFT;
case PUBLISHED;
case ARCHIVED;
/**
* @return non-empty-string
*/
public function get(): string
{
return $this->name;
}
}
',
'assertions' => [],
'ignored_issues' => [],
'php_version' => '8.1'
],
'valuePropertyFromInside' => [
'code' => '<?php
enum Status: string
{
case DRAFT = "draft";
case PUBLISHED = "published";
case ARCHIVED = "archived";
public function get(): string
{
return $this->value;
}
}
echo Status::DRAFT->get();
',
'assertions' => [],
'ignored_issues' => [],
'php_version' => '8.1'
],
'wildcardEnumAsParam' => [
'code' => '<?php
enum A {
case C_1;
case C_2;
case C_3;
/**
* @param self::C_* $i
*/
public static function foo(self $i) : void {}
}
A::foo(A::C_1);
A::foo(A::C_2);
A::foo(A::C_3);',
'assertions' => [],
'ignored_issues' => [],
'php_version' => '8.1',
],
'wildcardEnumAsReturn' => [
'code' => '<?php
enum E {
const A = 1;
case B;
}
/** @return E::* */
function f(): mixed {
return E::B;
}',
'assertions' => [],
'ignored_issues' => [],
'php_version' => '8.1',
],
'wildcardConstantsOnEnum' => [
'code' => '<?php
enum A {
const C_1 = 1;
const C_2 = 2;
const C_3 = 3;
/**
* @param self::C_* $i
*/
public static function foo(int $i) : void {}
}
A::foo(A::C_1);
A::foo(A::C_2);
A::foo(A::C_3);',
'assertions' => [],
'ignored_issues' => [],
'php_version' => '8.1',
],
'constantOfAVariableEnumClassString' => [
'code' => '<?php
enum A { const C = 3; }
$e = A::class;
$_z = $e::C;
',
'assertions' => [
'$_z===' => '3',
],
'ignored_issues' => [],
'php_version' => '8.1',
],
'constantOfAVariableEnumInstance' => [
'code' => '<?php
enum A {
const C = 3;
case AA;
}
$e = A::AA;
$_z = $e::C;
',
'assertions' => [
'$_z===' => '3',
],
'ignored_issues' => [],
'php_version' => '8.1',
],
2021-11-24 19:43:43 +01:00
'EnumCaseInAttribute' => [
'code' => '<?php
2021-11-24 19:43:43 +01:00
class CreateController {
#[Param(paramType: ParamType::FLAG)]
public function actionGet(): void {}
}
use Attribute;
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class Param {
public function __construct(
public ParamType $paramType = ParamType::PARAM
) {
}
}
enum ParamType {
case FLAG;
case PARAM;
}',
'assertions' => [],
'ignored_issues' => [],
'php_version' => '8.1',
2021-11-24 19:43:43 +01:00
],
'casesOnEnumWithNoCasesReturnEmptyArray' => [
'code' => '<?php
enum Status: int {}
$_z = Status::cases();
',
'assertions' => [
2021-12-15 12:20:31 +01:00
'$_z===' => 'array<never, never>',
],
'ignored_issues' => [],
'php_version' => '8.1',
],
'backedEnumFromReturnsInstanceOfThatEnum' => [
'code' => '<?php
enum Status: int {
case Open = 1;
case Closed = 2;
}
function f(): Status {
return Status::from(1);
}
',
'assertions' => [],
'ignored_issues' => [],
'php_version' => '8.1',
],
'backedEnumTryFromReturnsInstanceOfThatEnum' => [
'code' => '<?php
enum Status: int {
case Open = 1;
case Closed = 2;
}
function f(): Status {
return Status::tryFrom(rand(1, 10)) ?? Status::Open;
}
',
'assertions' => [],
'ignored_issues' => [],
'php_version' => '8.1',
],
'backedEnumFromReturnsSpecificCase' => [
'code' => '<?php
enum Status: int {
case Open = 1;
case Closed = 2;
}
$_z = Status::from(2);
',
'assertions' => [
'$_z===' => 'enum(Status::Closed)',
],
'ignored_issues' => [],
'php_version' => '8.1',
],
'backedEnumTryFromReturnsSpecificCase' => [
'code' => '<?php
enum Status: int {
case Open = 1;
case Closed = 2;
}
$_z = Status::tryFrom(2);
',
'assertions' => [
'$_z===' => 'enum(Status::Closed)|null',
],
'ignored_issues' => [],
'php_version' => '8.1',
],
'backedEnumFromReturnsUnionOfCases' => [
'code' => '<?php
enum Status: int {
case Open = 1;
case Closed = 2;
case Busted = 3;
}
$_z = Status::from(rand(1, 2));
',
'assertions' => [
'$_z===' => 'enum(Status::Closed)|enum(Status::Open)',
],
'ignored_issues' => [],
'php_version' => '8.1',
],
'backedEnumTryFromReturnsUnionOfCases' => [
'code' => '<?php
enum Status: int {
case Open = 1;
case Closed = 2;
case Busted = 3;
}
$_z = Status::tryFrom(rand(1, 2));
',
'assertions' => [
'$_z===' => 'enum(Status::Closed)|enum(Status::Open)|null',
],
'ignored_issues' => [],
'php_version' => '8.1',
],
2022-01-23 12:55:35 +01:00
'InterfacesWithProperties' => [
'code' => '<?php
2022-01-23 12:55:35 +01:00
static fn (\UnitEnum $tag): string => $tag->name;
static fn (\BackedEnum $tag): string|int => $tag->value;
2022-02-22 15:04:56 +01:00
interface ExtendedUnitEnum extends \UnitEnum {}
static fn (ExtendedUnitEnum $tag): string => $tag->name;
interface ExtendedBackedEnum extends \BackedEnum {}
static fn (ExtendedBackedEnum $tag): string|int => $tag->value;
2022-01-23 12:55:35 +01:00
',
'assertions' => [],
'ignored_issues' => [],
'php_version' => '8.1',
2022-01-23 12:55:35 +01:00
],
2022-02-12 21:40:51 +01:00
'EnumCollapsing' => [
'code' => '<?php
enum Code: int
{
case Ok = 0;
case Fatal = 1;
}
function foo(): int|Code|null
{
return null;
}
$code = foo();
if(!isset($code)){
$code = Code::Ok;
}',
'assertions' => [
'$code' => 'Code|int',
],
'ignored_issues' => [],
'php_version' => '8.1',
],
2022-02-13 19:05:51 +01:00
'EnumCaseReconciliation' => [
'code' => '<?php
enum Code: int
{
case Ok = 0;
case Fatal = 1;
}
function foo(): Code|null
{
return null;
}
$code = foo();
$code1 = null;
$code2 = null;
if($code instanceof Code){
$code1 = $code;
}
if(!$code instanceof Code){
$code2 = $code;
}',
'assertions' => [
'$code1' => 'Code|null',
'$code2' => 'null',
],
'ignored_issues' => [],
'php_version' => '8.1',
],
'constantAsACaseValueOfABackedEnum' => [
'code' => '<?php
enum Test: string
{
public const ENUM_VALUE = "forty two";
case TheAnswer = self::ENUM_VALUE;
}
$a = Test::TheAnswer->value;
',
'assertions' => [
'$a===' => "'forty two'",
],
'ignored_issues' => [],
'php_version' => '8.1',
],
];
}
/**
Add support for strict arrays, fix type alias intersection, fix array_is_list assertion on non-lists (#8395) * Immutable CodeLocation * Remove excess clones * Remove external clones * Remove leftover clones * Fix final clone issue * Immutable storages * Refactoring * Fixes * Fixes * Fix * Fix * Fixes * Simplify * Fixes * Fix * Fixes * Update * Fix * Cache global types * Fix * Update * Update * Fixes * Fixes * Refactor * Fixes * Fix * Fix * More caching * Fix * Fix * Update * Update * Fix * Fixes * Update * Refactor * Update * Fixes * Break one more test * Fix * FIx * Fix * Fix * Fix * Fix * Improve performance and readability * Equivalent logic * Fixes * Revert * Revert "Revert" This reverts commit f9175100c8452c80559234200663fd4c4f4dd889. * Fix * Fix reference bug * Make default TypeVisitor immutable * Bugfix * Remove clones * Partial refactoring * Refactoring * Fixes * Fix * Fixes * Fixes * cs-fix * Fix final bugs * Add test * Misc fixes * Update * Fixes * Experiment with removing different property * revert "Experiment with removing different property" This reverts commit ac1156e077fc4ea633530d51096d27b6e88bfdf9. * Uniform naming * Uniform naming * Hack hotfix * Clean up $_FILES ref #8621 * Undo hack, try fixing properly * Helper method * Remove redundant call * Partially fix bugs * Cleanup * Change defaults * Fix bug * Fix (?, hope this doesn't break anything else) * cs-fix * Review fixes * Bugfix * Bugfix * Improve logic * Add support for list{} and callable-list{} types, properly implement array_is_list assertions (fixes #8389) * Default to sealed arrays * Fix array_merge bug * Fixes * Fix * Sealed type checks * Properly infer properties-of and get_object_vars on final classes * Fix array_map zipping * Fix tests * Fixes * Fixes * Fix more stuff * Recursively resolve type aliases * Fix typo * Fixes * Fix array_is_list assertion on keyed array * Add BC docs * Fixes * fix * Update * Update * Update * Update * Seal arrays with count assertions * Fix #8528 * Fix * Update * Improve sealed array foreach logic * get_object_vars on template properties * Fix sealed array assertion reconciler logic * Improved reconciler * Add tests * Single source of truth for test types * Fix tests * Fixup tests * Fixup tests * Fixup tests * Update * Fix tests * Fix tests * Final fixes * Fixes * Use list syntax only when needed * Fix tests * Cs-fix * Update docs * Update docs * Update docs * Update docs * Update docs * Document missing types * Update docs * Improve class-string-map docs * Update * Update * I love working on psalm :) * Keep arrays unsealed by default * Fixup tests * Fix syntax mistake * cs-fix * Fix typo * Re-import missing types * Keep strict types only in return types * argc/argv fixes * argc/argv fixes * Fix test * Comment-out valinor code, pinging @romm pls merge https://github.com/CuyZ/Valinor/pull/246 so we can add valinor to the psalm docs :)
2022-11-05 22:34:42 +01:00
*
*/
public function providerInvalidCodeParse(): iterable
{
return [
'enumValueIsNot' => [
'code' => '<?php
enum Suit: string {
case Hearts = "h";
case Diamonds = "d";
case Clubs = "c";
case Spades = "s";
}
if (Suit::Hearts->value === "a") {}',
'error_message' => 'TypeDoesNotContainType',
'ignored_issues' => [],
'php_version' => '8.1'
],
'enumValueNotBacked' => [
'code' => '<?php
enum Suit {
case Hearts;
case Diamonds;
case Clubs;
case Spades;
}
echo Suit::Hearts->value;',
'error_message' => 'UndefinedPropertyFetch',
'ignored_issues' => [],
'php_version' => '8.1'
],
'badSuit' => [
'code' => '<?php
enum Suit {
case Hearts;
case Diamonds;
case Clubs;
case Spades;
}
function foo(Suit $s): void {
if ($s === Suit::Clu) {}
}',
'error_message' => 'UndefinedConstant',
'ignored_issues' => [],
'php_version' => '8.1'
],
'cantCompareToSuitTwice' => [
'code' => '<?php
enum Suit {
case Hearts;
case Diamonds;
case Clubs;
case Spades;
}
function foo(Suit $s): void {
if ($s === Suit::Clubs) {
if ($s === Suit::Clubs) {
echo "bad";
}
}
}',
'error_message' => 'RedundantCondition',
'ignored_issues' => [],
'php_version' => '8.1'
],
'insufficientMatches' => [
'code' => '<?php
enum Suit {
case Hearts;
case Diamonds;
case Clubs;
case Spades;
public function color(): string {
return match($this) {
Suit::Hearts, Suit::Diamonds => "Red",
Suit::Clubs => "Black",
};
}
}',
'error_message' => 'UnhandledMatchCondition',
'ignored_issues' => [],
'php_version' => '8.1'
],
'insufficientMatchesForCases' => [
'code' => '<?php
enum Suit {
case Hearts;
case Diamonds;
case Clubs;
case Spades;
}
foreach (Suit::cases() as $case) {
echo match($case) {
Suit::Hearts, Suit::Diamonds => "Red",
Suit::Clubs => "Black",
};
}',
'error_message' => 'UnhandledMatchCondition',
'ignored_issues' => [],
'php_version' => '8.1'
],
'invalidBackingType' => [
'code' => '<?php
enum Status: array {}
',
'error_message' => 'InvalidEnumBackingType',
'ignored_issues' => [],
'php_version' => '8.1',
],
'invalidCaseTypeForBackedEnum' => [
'code' => '<?php
enum Status: int {
case Open = [];
}
',
'error_message' => 'InvalidEnumCaseValue',
'ignored_issues' => [],
'php_version' => '8.1',
],
'duplicateValues' => [
'code' => '<?php
enum Status: string
{
case Foo = "foo";
case Bar = "bar";
case Baz = "bar";
}
',
'error_message' => 'DuplicateEnumCaseValue',
'ignored_issues' => [],
'php_version' => '8.1',
],
'duplicateCases' => [
'code' => '<?php
enum Status
{
case Foo;
case Foo;
}
',
'error_message' => 'DuplicateEnumCase',
'ignored_issues' => [],
'php_version' => '8.1',
],
'caseWithAValueOfANonBackedEnum' => [
'code' => '<?php
enum Status
{
case Foo = 1;
}
',
'error_message' => 'InvalidEnumCaseValue',
'ignored_issues' => [],
'php_version' => '8.1',
],
'caseWithoutAValueOfABackedEnum' => [
'code' => '<?php
enum Status: int
{
case Foo;
}
',
'error_message' => 'InvalidEnumCaseValue',
'ignored_issues' => [],
'php_version' => '8.1',
],
'caseTypeMismatch' => [
'code' => '<?php
enum Status: int
{
case Foo = "one";
}
',
'error_message' => 'InvalidEnumCaseValue',
'ignored_issues' => [],
'php_version' => '8.1',
],
'propsOnEnum' => [
'code' => '<?php
enum Status {
public $prop;
}
',
'error_message' => 'NoEnumProperties',
'ignored_issues' => [],
'php_version' => '8.1',
],
'enumInstantiation' => [
'code' => '<?php
enum Status {}
new Status;
',
'error_message' => 'UndefinedClass',
'ignored_issues' => [],
'php_version' => '8.1',
],
'enumsAsAttributes' => [
'code' => '<?php
#[Attribute(Attribute::TARGET_CLASS)]
enum Status { }
',
'error_message' => 'InvalidAttribute',
'ignored_issues' => [],
'php_version' => '8.1',
],
'deprecatedAttribute' => [
'code' => '<?php
enum Foo {
case A;
#[Psalm\Deprecated]
case B;
}
Foo::B;
',
'error_message' => 'DeprecatedConstant',
'ignored_issues' => [],
'php_version' => '8.1',
],
];
}
}