Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support class const covariance checks #5589

Closed
AndrolGenhald opened this issue Apr 6, 2021 · 17 comments · Fixed by #7154
Closed

Support class const covariance checks #5589

AndrolGenhald opened this issue Apr 6, 2021 · 17 comments · Fixed by #7154

Comments

@AndrolGenhald
Copy link
Collaborator

AndrolGenhald commented Apr 6, 2021

Non-private class constants should be covariant, so a child can't change the type of a parent's const: https://psalm.dev/r/e287a07e3f

Same reasoning as #4184, but since consts can't be changed there's no need for contravariance, hence they should be covariant rather than invariant.

@var or @const should also be used to specify the type (@var seems wrong but seems to be what's used other places).

@psalm-github-bot
Copy link

I found these snippets:

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

class A {
    /**
     * @var array
     *
     * It's a const, not a variable, but there should be some
     * way to tell psalm that children must keep ARR as an array
     */
    public const ARR = [];
    
    public static function foo(): void
    {
        foreach (static::ARR as $_key => $_val) {
        }
    }
}

class B extends A {
    public const ARR = "";
}
Psalm output (using commit a469c82):

No issues!

@orklah
Copy link
Collaborator

orklah commented Aug 10, 2021

blocked by #942

@orklah
Copy link
Collaborator

orklah commented Oct 23, 2021

Not sure they should be covariant though. Constant inheritance is a thing, so it seems like they should be invariant, like properties

@AndrolGenhald
Copy link
Collaborator Author

I believe properties must be invariant because a parent method can be called on a child class and change a child property, potentially violating its type on the child (eg parent defines property as array, child defines it as array<int>, parent method assigns array<string> to the property). Since constants can't be changed, there's no risk of the parent method changing it and violating the type given in the child.

To quote the first paragraph of the issue I linked:

Non-private properties in child classes need to be covariant for reading, like function returns and simultaneously contravariant for writing, like function params. That adds up to them needing to be invariant, or have exactly the same type as the property in the parent class.

Consts also need to be covariant for reading, but since they can't be written they have no need for contravariance.

@orklah
Copy link
Collaborator

orklah commented Oct 24, 2021

Then what about
https://psalm.dev/r/63d21b3f4c
https://3v4l.org/HKIQH
?

@psalm-github-bot
Copy link

I found these snippets:

https://psalm.dev/r/63d21b3f4c
<?php

class A{
    const C = 'hello';
    
    public static function run(): void{
     	echo strpos(static::C, 'e');   
    }
}

class B extends A{
    const C = [];
    
}

B::run();
Psalm output (using commit 5500ce5):

INFO: MixedArgument - 7:19 - Argument 1 of strpos cannot be mixed, expecting string

@AndrolGenhald
Copy link
Collaborator Author

AndrolGenhald commented Oct 24, 2021

Yeah, that should show an error because it's not covariant. Child classes should be allowed to define more strict requirements, but not less string requirements for the type. Basically, if it works for return types, it should work for consts.

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

Looks like I need to open a separate issue for argument contravariance checks being missing. Edit: nevermind I'm an idiot, I screwed up the phpdoc for that one.

@psalm-github-bot
Copy link

psalm-github-bot bot commented Oct 24, 2021

I found these snippets:

https://psalm.dev/r/7e497434f3
<?php

abstract class A
{
    /** @var non-empty-string */
    public const ERROR_CONST_CONTRAVARIANT = "str";
    
    /** @var string */
    public const CONST_COVARIANT = "";
    
    /** @var string */
    public $errorPropertyCovariant;
    
    /** @var non-empty-string */
    public $errorPropertyContravariant;
    
    /** @var string */
    public $propertyInvariant;
    
    /** @param string $arg */
    abstract public function errorArgumentNotContravariant(string $arg): string;
    
    /** @param non-empty-string $arg */
    abstract public function argumentContravariant(string $arg): string;
    
    /** @return non-empty-string */
    abstract public function errorReturnTypeNotCovariant(): string;
    
    /** @return string */
    abstract public function returnTypeCovariant(): string;
}

abstract class B extends A
{
    /** @var string */
    public const ERROR_CONST_CONTRAVARIANT = "";
    
    /** @var non-empty-string */
    public const CONST_COVARIANT = "";
    
    /** @var non-empty-string */
    public $errorPropertyCovariant;
    
    /** @var string */
    public $errorPropertyContravariant;
    
    /** @var string */
    public $propertyInvariant;
    
    /** @param non-empty-string $arg */
    abstract public function errorArgumentNotContravariant(string $arg): string;
    
    /** @param string $arg */
    abstract public function argumentContravariant(string $arg): string;
    
    /** @return string */
    abstract public function errorReturnTypeNotCovariant(): string;
    
    /** @return non-empty-string */
    abstract public function returnTypeCovariant(): string;
}
Psalm output (using commit e334923):

ERROR: NonInvariantDocblockPropertyType - 42:12 - Property B::$errorPropertyCovariant has type non-empty-string, not invariant with A::$errorPropertyCovariant of type string

ERROR: NonInvariantDocblockPropertyType - 45:12 - Property B::$errorPropertyContravariant has type string, not invariant with A::$errorPropertyContravariant of type non-empty-string

ERROR: MoreSpecificImplementedParamType - 51:67 - Argument 1 of B::errorArgumentNotContravariant has the more specific type 'non-empty-string', expecting 'string' as defined by A::errorArgumentNotContravariant

ERROR: LessSpecificImplementedReturnType - 56:17 - The inherited return type 'non-empty-string' for A::errorReturnTypeNotCovariant is more specific than the implemented return type for B::errorreturntypenotcovariant 'string'

AndrolGenhald added a commit to AndrolGenhald/psalm that referenced this issue Dec 13, 2021
Add class const covariance support (fixes vimeo#5589).
Add check for overriding const from interface in PHP < 8.1 (fixes vimeo#7108).
Add check for ambiguous const inheritance.
@orklah
Copy link
Collaborator

orklah commented Jan 22, 2022

Right, I had a slight déja-vu about this discussion :)

I'm not sure I follow your argument that class constants should behave like return types.

They're more related to property types, no? And those are invariants

(I have a possible legit explanation why we should consider them covariant, but I need to make sure I'm not just rationalizing here)

@AndrolGenhald
Copy link
Collaborator Author

@orklah My argument is here. Since properties can be written there's a risk of passing a child to a function taking the parent as an argument, and that function could change the property to something that matches the parent type but not the child type. Constants can't be written so there's no such risk.

If I'm right, the same also applies to read-only properties, they could be made covariant, but I don't care enough to do it personally.

@orklah
Copy link
Collaborator

orklah commented Jan 22, 2022

Ok, I arrived to the same conclusion, I'm convinced :)

I didn't understood the last time

AndrolGenhald added a commit to AndrolGenhald/psalm that referenced this issue Jan 25, 2022
Add class const covariance support (fixes vimeo#5589).
Add check for overriding const from interface in PHP < 8.1 (fixes vimeo#7108).
Add check for ambiguous const inheritance.
AndrolGenhald added a commit to AndrolGenhald/psalm that referenced this issue Jan 25, 2022
Add class const covariance support (fixes vimeo#5589).
Add check for overriding const from interface in PHP < 8.1 (fixes vimeo#7108).
Add check for ambiguous const inheritance.
@muglug
Copy link
Collaborator

muglug commented Jan 30, 2022

Looks like this is now caught?

@AndrolGenhald
Copy link
Collaborator Author

AndrolGenhald commented Jan 31, 2022

Yes, looks like GitHub stopped recognizing "fixes #..." comments recently. Maybe it's because things are being merged to master which is no longer the default branch?

@AndrolGenhald
Copy link
Collaborator Author

AndrolGenhald commented Jan 31, 2022

@orklah Since you changed literal strings to use single quotes, maybe types should be quoted with double quotes now? https://psalm.dev/r/e287a07e3f
Also, I should probably add InvalidClassConstantType or something, I'm guessing LessSpecific is correct for most cases this issue will show up, but '' certainly isn't less specific than array, it's completely different.

@psalm-github-bot
Copy link

I found these snippets:

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

class A {
    /**
     * @var array
     *
     * It's a const, not a variable, but there should be some
     * way to tell psalm that children must keep ARR as an array
     */
    public const ARR = [];
    
    public static function foo(): void
    {
        foreach (static::ARR as $_key => $_val) {
        }
    }
}

class B extends A {
    public const ARR = "";
}
Psalm output (using commit e3050b1):

INFO: LessSpecificClassConstantType - 20:18 - The type '''' for B::ARR is more general than the type 'array<array-key, mixed>' inherited from A::ARR

@orklah
Copy link
Collaborator

orklah commented Jan 31, 2022

Yeah, that could be great

@bdsl
Copy link
Contributor

bdsl commented Feb 3, 2022

I think the same probably doesn't apply generally to read-only properties. Read-only really means write-once. If it's not contravariant there still seems to be a risk that the parent could write it before the child does, and set it to something not assignable to the type defined in the child.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants