psalm icon indicating copy to clipboard operation
psalm copied to clipboard

[Help wanted] Listing PHP 8.1 features

Open orklah opened this issue 2 years ago • 27 comments

The first release candidate for PHP 8.1 is out now and the feature freeze is past, We may start looking at the new features and ensuring they're implemented in Psalm. Some were already implemented by Matt but not all.

This thread will regroup every new feature to come and what impact it will have on Psalm.

PHP release notes can be found here: https://github.com/php/php-src/blob/PHP-8.1/UPGRADING.

If you want to help, please respond here with the feature name, a small reproducer of the feature(and the impact it must have on Psalm analysis). For example:

  • [x] array_is_list type inference: https://psalm.dev/r/fe157fd547
  • [x] array_is_list impossible type: https://psalm.dev/r/bb9d8800b1
  • [x] readonly keyword: https://psalm.dev/r/ec599a194c
  • [x] string key array unpacking support: https://psalm.dev/r/a25339af39

Callmap/stub changes

  • [x] array_is_list function : https://psalm.dev/r/8bef57c13a -> fixed: #6398
  • [ ] #6401
  • [x] #6402
  • [x] #6403
  • [x] #6404
  • [x] #6405
  • [x] #6406
  • [x] #6407
  • [x] #6408
  • [x] #6414
  • [x] #6415
  • [x] #6416
  • [x] #6417
  • [x] #6418
  • [x] #6422
  • [x] #6731

Parser changes

  • [x] #6410
  • [x] #6412
  • [x] #6413
  • [x] #7668

Enum support

  • [x] #6425

I'll update this post with new items as well as progress

orklah avatar Sep 03 '21 18:09 orklah

I found these snippets:

https://psalm.dev/r/8bef57c13a
<?php

array_is_list([]);
Psalm output (using commit a655ca8):

ERROR: UndefinedFunction - 3:1 - Function array_is_list does not exist
https://psalm.dev/r/fe157fd547
<?php

$a = [];
if(array_is_list($a)) {
 	/** @psalm-trace $a */
    //$a should be `list<mixed>` here
}
Psalm output (using commit a655ca8):

ERROR: UndefinedFunction - 4:4 - Function array_is_list does not exist

INFO: Trace - 6:0 - $a: array<array-key, mixed>
https://psalm.dev/r/bb9d8800b1
<?php

$a = ['a' => 5];
if(array_is_list($a)) { //Psalm should emit an issue because the types don't match
 	
}
Psalm output (using commit a655ca8):

ERROR: UndefinedFunction - 4:4 - Function array_is_list does not exist

psalm-github-bot[bot] avatar Sep 03 '21 18:09 psalm-github-bot[bot]

I'm thinking about managing readonly in promoted constructors: https://psalm.dev/r/ec599a194c

I'm using this blog to have a list of new features and deprecated stuff in PHP 8.1. You could probably extract a list from this, as it regroups a lot.

niconoe- avatar Sep 03 '21 22:09 niconoe-

I found these snippets:

https://psalm.dev/r/ec599a194c
<?php

class TheReadonlyFeature
{
    public function __construct(
        public readonly string $bar
    ) {}
}

new TheReadonlyFeature('string');
Psalm output (using commit a655ca8):

ERROR: ParseError - 6:25 - Syntax error, unexpected T_STRING, expecting T_VARIABLE on line 6

ERROR: TooManyArguments - 10:1 - Too many arguments for TheReadonlyFeature::__construct - expecting 0 but saw 1

ERROR: InvalidDocblock - 6:9 - Param1 of TheReadonlyFeature::__construct has invalid syntax

psalm-github-bot[bot] avatar Sep 03 '21 22:09 psalm-github-bot[bot]

@niconoe- thanks, I added your case. Yeah, I know where to find the new features, but creating every repro, thinking of impact on Psalm would take me a long time. It's a good opportunity to make the community participate, even when they lack technical insight of Psalm.

Ideally, I'll then create issues for the simpler cases with an explanation how to do it and encourage people to try and implement the feature

orklah avatar Sep 04 '21 05:09 orklah

Array unpacking with string keys should be supported and new array type inferred

https://psalm.dev/r/a25339af39

simPod avatar Sep 04 '21 10:09 simPod

I found these snippets:

https://psalm.dev/r/a25339af39
<?php

$x = [
    "a" => 0,
    ...["a" => 1],
    ...["b" => 2]
];

/** @psalm-trace $x */
// $x should be ['a' => 1, 'b' => 2] as `a` is overwritten and `b` added.
Psalm output (using commit a655ca8):

ERROR: DuplicateArrayKey - 5:8 - String keys are not supported in unpacked arrays

ERROR: DuplicateArrayKey - 6:8 - String keys are not supported in unpacked arrays

INFO: Trace - 10:74 - $x: array{a: 0}

INFO: UnusedVariable - 3:1 - $x is never referenced or the value is not used

psalm-github-bot[bot] avatar Sep 04 '21 10:09 psalm-github-bot[bot]

Thanks @simPod!

orklah avatar Sep 04 '21 10:09 orklah

can you tag this with "help wanted" and pin the issue?

Done. I also added milestone that we can assign to issues / PRs.

weirdan avatar Sep 04 '21 11:09 weirdan

With Enum:

  • Already used case: https://psalm.dev/r/ceaa9c9892
  • Already used value for several cases: https://psalm.dev/r/108275cc1c
  • Only string and int are allowed types as enum values: https://psalm.dev/r/89eb27ab21
  • Implement callmap Enum::from, Enum::tryFrom and Enum::cases: https://psalm.dev/r/3a77b77825
  • Enum are objects: https://psalm.dev/r/d153c9d4f0
  • Enum's reflection and automatically defined stuff: https://psalm.dev/r/ec911f5510
  • New function enum_exists: https://psalm.dev/r/9b5935609d
  • Attributes are allowed on Enums and TARGET_CLASS also references Enums (I don't know how to explain this with a snippet)

I think that's already something 😄

[@weirdan] Split into a separate ticket here: https://github.com/vimeo/psalm/issues/6425

niconoe- avatar Sep 05 '21 13:09 niconoe-

I found these snippets:

https://psalm.dev/r/ceaa9c9892
<?php

enum Status {
  case Foo;
  case Bar;
  case Bar;
}
Psalm output (using commit 916b098):

ERROR: ParseError - 3:6 - Syntax error, unexpected T_STRING on line 3
https://psalm.dev/r/108275cc1c
<?php

enum Status: string
{
    case Foo = 'foo';
    case Bar = 'bar';
    case Baz = 'bar';
}
Psalm output (using commit 916b098):

ERROR: ParseError - 3:6 - Syntax error, unexpected T_STRING on line 3

ERROR: ParseError - 5:5 - Syntax error, unexpected T_CASE on line 5
https://psalm.dev/r/89eb27ab21
<?php

enum Status: array
{
    case IsEmpty = [];
    case IsNotEmpty = ['foo'];
}
Psalm output (using commit 916b098):

ERROR: ParseError - 3:6 - Syntax error, unexpected T_STRING on line 3

ERROR: ParseError - 4:1 - Syntax error, unexpected '{', expecting '(' on line 4

ERROR: ParseError - 6:5 - Syntax error, unexpected T_CASE on line 6

ERROR: UndefinedConstant - 3:1 - Const enum is not defined

ERROR: UndefinedConstant - 5:10 - Const IsEmpty is not defined

ERROR: UndefinedConstant - 6:10 - Const IsNotEmpty is not defined
https://psalm.dev/r/3a77b77825
<?php

enum Status: int
{
    case FOO = 1;
    case BAR = 2;
}

// Returns Status::FOO
Status::from(1);

// Throw ValueError Exception
Status::from(3);

// Retunrs null
Status::tryFrom(3);

// Returns [Status::FOO, Status::BAR]
Status::cases();
Psalm output (using commit 916b098):

ERROR: ParseError - 3:6 - Syntax error, unexpected T_STRING on line 3

ERROR: ParseError - 5:5 - Syntax error, unexpected T_CASE on line 5
https://psalm.dev/r/d153c9d4f0
<?php

enum Status: int
{
    case FOO = 1;
    case BAR = 2;
}

$statusA = Status::FOO;
$statusB = Status::FOO;
$statusC = Status::BAR;

$statusA === $statusB; // true
$statusA === $statusC; // false
$statusC instanceof Status; // true
Psalm output (using commit 916b098):

ERROR: ParseError - 3:6 - Syntax error, unexpected T_STRING on line 3

ERROR: ParseError - 5:5 - Syntax error, unexpected T_CASE on line 5
https://psalm.dev/r/ec911f5510
<?php

enum Status: int
{
    case FOO;
    case BAR = 42;
}

$reflectedEnum = new ReflectionEnum(Status::class);

$reflectedEnum->hasCase('FOO'); //returns true
$reflectedEnum->hasCase('BAZ'); //returns false

$reflectedEnum->getCases(); //returns array<ReflectionEnumPureCase|ReflectionEnumBackedCase>

$reflectedEnum->getCase('FOO'); //returns ReflectionEnumPureCase
$reflectedEnum->getCase('BAR'); //returns ReflectionEnumBackedCase
$reflectedEnum->getCase('BAZ'); //throws ReflectionException

$reflectedEnum->isBacked('FOO'); //returns false
$reflectedEnum->isBacked('BAR'); //returns true
$reflectedEnum->isBacked('BAZ'); //Unknown behavior? returns false or throw ReflectionException? RFC don't say.

$reflectedEnum->getBackingType('FOO'); //returns null
$reflectedEnum->getBackingType('BAR'); //returns ReflectionType<int|string> (in this example, ReflectionType<int>)
$reflectedEnum->getBackingType('BAZ'); //Unknown behavior? returns null or throw ReflectionException? RFC don't say.

// Also exists:
$reflectedUnitEnum = new ReflectionEnumUnitCase(Status::FOO);
$reflectedUnitEnum->getValue(); //returns Status::FOO
$reflectedUnitEnum->getEnum(); //returns ReflectionEnum<Status>

$reflectedBackedEnum = new ReflectionEnumBackedCase (Status::BAR);
$reflectedBackedEnum->getValue(); //returns Status::BAR
$reflectedBackedEnum->getEnum(); //returns ReflectionEnum<Status>
$reflectedBackedEnum->getBackingValue(); //returns int|string (in this example, 42)

// Also possible:
$foo = Status::FOO;
$bar = Status::BAR;
$foo->name === 'FOO'; // Automatically defined readonly property.
$bar->name === 'BAR'; // Automatically defined readonly property.

// Finally:
enum_exists(Status::FOO); //returns true
enum_exists(Status::BAZ); //returns false
Psalm output (using commit 916b098):

ERROR: ParseError - 3:6 - Syntax error, unexpected T_STRING on line 3

ERROR: ParseError - 5:5 - Syntax error, unexpected T_CASE on line 5
https://psalm.dev/r/9b5935609d
<?php

enum Status: int
{
    case FOO;
    case BAR = 42;
}

enum_exists(Status::FOO); //returns true
enum_exists(Status::BAZ); //returns false
Psalm output (using commit 916b098):

ERROR: ParseError - 3:6 - Syntax error, unexpected T_STRING on line 3

ERROR: ParseError - 5:5 - Syntax error, unexpected T_CASE on line 5

psalm-github-bot[bot] avatar Sep 05 '21 13:09 psalm-github-bot[bot]

Enum are objects

Can you elaborate? It seems Psalm already knows that: https://psalm.dev/r/a57335dce7?php=8.1

weirdan avatar Sep 05 '21 13:09 weirdan

I found these snippets:

https://psalm.dev/r/a57335dce7
<?php

enum Status: int
{
    case FOO = 1;
    case BAR = 2;
}


$statusC = Status::BAR;

if ($statusC instanceof Status) {}
Psalm output (using commit 916b098):

ERROR: RedundantCondition - 12:5 - Type enum(Status::BAR) for $statusC is always Status

psalm-github-bot[bot] avatar Sep 05 '21 13:09 psalm-github-bot[bot]

Enum are objects

Can you elaborate? It seems Psalm already knows that: https://psalm.dev/r/a57335dce7?php=8.1

Sorry, I didn't know I could test against PHP 8.1 using the GET parameter, I was tesing on PHP 8.0 (or default version). You can remove this then, as it's already implemented.

niconoe- avatar Sep 05 '21 14:09 niconoe-

I found these snippets:

https://psalm.dev/r/a57335dce7
<?php

enum Status: int
{
    case FOO = 1;
    case BAR = 2;
}


$statusC = Status::BAR;

if ($statusC instanceof Status) {}
Psalm output (using commit 916b098):

ERROR: RedundantCondition - 12:5 - Type enum(Status::BAR) for $statusC is always Status

psalm-github-bot[bot] avatar Sep 05 '21 14:09 psalm-github-bot[bot]

Overriding final class constants should return error https://psalm.dev/r/867638ab8b?php=8.1

SpartakusMd avatar Oct 08 '21 06:10 SpartakusMd

I found these snippets:

https://psalm.dev/r/867638ab8b
<?php

class A
{
    final const BAR = 'BAZ';
}

class B extends A {
    const BAR = 'BUZZ';
}
Psalm output (using commit f35df42):

INFO: UnusedClass - 8:7 - Class B is never used

psalm-github-bot[bot] avatar Oct 08 '21 06:10 psalm-github-bot[bot]

In PHP 8.1 there are now deprecation notices when extending a class without specifying its types.

Running

class Foo implements Iterator {
    public function current() {
        return 'foo';
    }
    
    public function next() {}
    
    public function key() {}
    
    public function valid() {
        return true;
    }
    
    public function rewind() {}
}

produces

Deprecated: Return type of Foo::current() should either be compatible with Iterator::current(): mixed, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /in/To1Ym on line 6

Deprecated: Return type of Foo::next() should either be compatible with Iterator::next(): void, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /in/To1Ym on line 10

<... more ...>

I've created a branch to add a lot of these return types: https://github.com/vimeo/psalm/compare/muglug-hard-typed-generic-stubs

I think some extra care will be needed when scanning in PHP < 8.1 so that we don't force implementers to use those types (especially where they're not supported, like mixed.

muglug avatar Oct 26 '21 15:10 muglug

Those things that are merged already (outlined in OP), are already available when setting phpVersion="8.1" now?

kkmuffme avatar Nov 09 '21 13:11 kkmuffme

@kkmuffme yes. Some may be available in master only though, but most are already a part of some release.

weirdan avatar Nov 09 '21 13:11 weirdan

Match statements seem to be partially supported, only. In a switch statement, if I switch on the type of an object, psalm understands it has that type in the case blocks. For a match statement, it does not.

GrahamCampbell avatar Apr 11 '22 10:04 GrahamCampbell

Could you provide a snippet please?

orklah avatar Apr 11 '22 12:04 orklah

Actually, both have issues:

<?php

class Foo
{
    function eg1(): bool { return false; }
}

class Bar
{
    function eg2(): bool { return false; }
}

class Baz
{
    function eg2(): bool { return false; }
}

function f(Foo|Bar|Baz $x): bool
{
    switch ($x::class) {
        case Foo::class:
            return $x->eg1();
        case Bar::class:
        case Baz::class:
            return $x->eg2();
    }
}

https://psalm.dev/r/98e758a602

<?php

class Foo
{
    function eg1(): bool { return false; }
}

class Bar
{
    function eg2(): bool { return false; }
}

class Baz
{
    function eg2(): bool { return false; }
}

function f(Foo|Bar|Baz $x): bool
{
    return match ($x::class) {
        Foo::class => $x->eg1(),
        Bar::class, Baz::class => $x->eg2(),
    };
}

https://psalm.dev/r/7d1f1bd52b

GrahamCampbell avatar Apr 11 '22 13:04 GrahamCampbell

I found these snippets:

https://psalm.dev/r/98e758a602
<?php

class Foo
{
    function eg1(): bool { return false; }
}

class Bar
{
    function eg2(): bool { return false; }
}

class Baz
{
    function eg2(): bool { return false; }
}

function f(Foo|Bar|Baz $x): bool
{
    switch ($x::class) {
        case Foo::class:
            return $x->eg1();
        case Bar::class:
        case Baz::class:
            return $x->eg2();
    }
}
Psalm output (using commit 916fddb):

ERROR: InvalidReturnType - 18:29 - Not all code paths of f end in a return statement, return type bool expected
https://psalm.dev/r/7d1f1bd52b
<?php

class Foo
{
    function eg1(): bool { return false; }
}

class Bar
{
    function eg2(): bool { return false; }
}

class Baz
{
    function eg2(): bool { return false; }
}

function f(Foo|Bar|Baz $x): bool
{
    return match ($x::class) {
        Foo::class => $x->eg1(),
        Bar::class, Baz::class => $x->eg2(),
    };
}
Psalm output (using commit 916fddb):

ERROR: PossiblyUndefinedMethod - 22:39 - Method Foo::eg2 does not exist

INFO: MixedReturnStatement - 20:12 - Possibly-mixed return value

INFO: MixedReturnStatement - 20:12 - Could not infer a return type

INFO: MixedInferredReturnType - 18:29 - Could not verify return type 'bool' for f

psalm-github-bot[bot] avatar Apr 11 '22 13:04 psalm-github-bot[bot]

Yeah, explaining to Psalm that mapping $x::class values with specific literals will make $x type change is possibly non-trivial, though it works with conditionals: https://psalm.dev/r/ae73672517

Could you create a new issue for that? It's not really related to PHP 8.1 if switch has the same issue

orklah avatar Apr 11 '22 22:04 orklah

I found these snippets:

https://psalm.dev/r/ae73672517
<?php

class Foo
{
    function eg1(): bool { return false; }
}

class Bar
{
    function eg2(): bool { return false; }
}

class Baz
{
    function eg2(): bool { return false; }
}

function f(Foo|Bar|Baz $x): bool
{
    if ($x::class === Foo::class) {
            return $x->eg1();
    }
}
Psalm output (using commit 916fddb):

ERROR: InvalidReturnType - 18:29 - Not all code paths of f end in a return statement, return type bool expected

psalm-github-bot[bot] avatar Apr 11 '22 22:04 psalm-github-bot[bot]

Switch has a different issue. It understands the type, but doesn't understand the matching is complete.

GrahamCampbell avatar Apr 11 '22 22:04 GrahamCampbell

Pending https://github.com/vimeo/psalm/issues/8374

kkmuffme avatar Aug 05 '22 14:08 kkmuffme

This is pretty much done.

weirdan avatar Mar 03 '23 21:03 weirdan