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

feat(types): add properties-of<T> type #7359

Merged
merged 9 commits into from Feb 28, 2022
Merged

feat(types): add properties-of<T> type #7359

merged 9 commits into from Feb 28, 2022

Conversation

Patrick-Remy
Copy link
Contributor

@Patrick-Remy Patrick-Remy commented Jan 9, 2022

For ActiveRecord-patterns in php, there is currently no possibility to type methods like asArray() . E.g. for the Yii framework:

/**
 * @property int $a
 * @property bool $b
 */
class Foo extends \yii\db\ActiveRecord {
}

// this will have type `array{a: string, b: string}[]`, as db will return all values as strings in Yii
$foo = Foo::find()->asArray()->all();

This PR adds properties-of<T> type that currently returns all class property names. There are the variants public-properties-of, protected-properties-of and private-properties-of.

I also fixed an imho bug, that the original class name wasn't proper downpassed in ReturnTypeAnalyzer so that static wasn't properly resolved in TypeExpander.

I aim in a second iteration to return an array-shape form of the class properties, instead of only the names, so that for the above exampleproperties-of<Foo> equals array{a: int} instead of currently only 'a'. So that key-of<class-properties<Foo>> will pick all property names. Unfortunately currently key-of only supports constants.

TODO

SKIPPED

  • T[k] usable for arrays, e.g. (array{a: string})[a] = string
  • Add type TLiteralProperty for further checks like property_exists

Please note, that this is my very first contribution to psalm, so please be aware of any inconveniences. ;)

Partially fixes #3141

@orklah
Copy link
Collaborator

orklah commented Jan 9, 2022

Firstly, congrats! This is a lot of work and it's not the easiest way to begin in Psalm!

Secondly, I have some remarks and checks to add:

  • Here is a small snippet with a lot of things to check and where we should have answers: https://psalm.dev/r/4971520a7d
  • I think it could make sense to have a TLiteralPropertyOf, the same way we have TLiteralClassString for example. This could be used for two things: "Transforming" literal strings into checked properties, for example, after a property_exists check and also resolving literal properties when they are few enough. For example on this: https://psalm.dev/r/9e2eb788b2
  • I wonder if we could make TPropertiesOf a child of TNonFalsyString, that way it will behave like a string in most cases
  • I think it will be easier to add this into Psalm 5 (so in master). I already see two places in your PR that will create conflicts with changes we made
  • Could you submit your fix in another PR (4.x this time)? It should make you a known contributor and you won't need us anymore to run the CI

@psalm-github-bot
Copy link

I found these snippets:

https://psalm.dev/r/4971520a7d
<?php

class A{
 	public string $foo;   
    private int $bar;
}

/**
 * @param properties-of<A> $a
 * @param private-properties-of<A> $b
 */
function scope($a, string $d){
	$b = new A();
    $c = $b->$a; //what happens to $c?
    $b->$a = 12; //will Psalm detect that type error?
    assert(is_string($a)); // will that pass? what will be the type of $a afterwards?
    $b->$d = 5; // the private access should be detected
    property_exists($b, $a);// this should be flagged as redundant
}
Psalm output (using commit 206332b):

ERROR: UndefinedDocblockClass - 9:11 - Docblock-defined class, interface or enum named properties-of does not exist

INFO: MissingReturnType - 12:10 - Method scope does not have a return type, expecting void

ERROR: MissingConstructor - 4:17 - A has an uninitialized property A::$foo, but no constructor

ERROR: MissingConstructor - 5:17 - A has an uninitialized property A::$bar, but no constructor
https://psalm.dev/r/9e2eb788b2
<?php

class A{
 	public string $foo;   
    private int $bar;
    private int $bar2;
}

/**
 * @param properties-of<A> $a
 */
function scope($a){
	$b = match($a) {
     	'foo' => null,
		default => $a        
    };
    
    if($b === 'bar'){ // this should be allowed
        
    } elseif($b = 'foo') { //this should not because foo was removed
        
    }
}
Psalm output (using commit 206332b):

ERROR: UndefinedDocblockClass - 10:11 - Docblock-defined class, interface or enum named properties-of does not exist

INFO: MissingReturnType - 12:10 - Method scope does not have a return type, expecting void

ERROR: MissingConstructor - 4:17 - A has an uninitialized property A::$foo, but no constructor

ERROR: MissingConstructor - 5:17 - A has an uninitialized property A::$bar, but no constructor

ERROR: MissingConstructor - 6:17 - A has an uninitialized property A::$bar2, but no constructor

src/Psalm/Internal/Type/TypeExpander.php Outdated Show resolved Hide resolved
src/Psalm/Internal/Type/TypeExpander.php Outdated Show resolved Hide resolved
@Patrick-Remy Patrick-Remy changed the base branch from 4.x to master February 21, 2022 22:06
@Patrick-Remy Patrick-Remy changed the title WIP feat(types): add properties-of<T> type feat(types): add properties-of<T> type Feb 22, 2022
@Patrick-Remy
Copy link
Contributor Author

Patrick-Remy commented Feb 22, 2022

@orklah I'm done for now and this should be ready for review. I reworked this PR but I think the changes are clear if you have a look at the tests.

I will initially skip TLiteralProperty as it's a bit more work to do, but I will raise another PR for it, once this is merged.

Probably only the lack of template support is currently a bit bad, but from key-of/value-of I know that is also very tricky and error-prone.

@orklah orklah added PR: Need review release:feature The PR will be included in 'Features' section of the release notes and removed PR: Needs work labels Feb 22, 2022
@@ -0,0 +1,40 @@
# Utility types
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where are key-of and value-of documented? Maybe they should be added here? Maybe SomeClass::CONST_* as well? Probably others?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can add these as well in another PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added key-of and value-of, SomeClass::CONST_* also could need some documentation, but I think it's not a utility type if you look at typescripts definition (https://www.typescriptlang.org/docs/handbook/utility-types.html)

@Patrick-Remy
Copy link
Contributor Author

Template/Generic support was easier than I thought! It's done and documented :)

@orklah
Copy link
Collaborator

orklah commented Feb 24, 2022

Seems fine to me :)

@ondrejmirtes FYI, this PR introduces a few types in Psalm, namely properties-of<T> and variants like private-properties-of<T>. Also it extends key-of<ClassName::CONSTNAME> and to work with template value-of<ClassName::CONSTNAME> to work with templates too

@Patrick-Remy
Copy link
Contributor Author

Patrick-Remy commented Feb 24, 2022

Template Support for key-of and value-of was introduced in #7359
I only documented those changes as requested, and extended them to work well with properties-of

@orklah
Copy link
Collaborator

orklah commented Feb 24, 2022

You linked this PR :)

@Patrick-Remy
Copy link
Contributor Author

@AndrolGenhald do you have some time for review? I'd love to see this in a next alpha of 5.0 🙂

@orklah orklah merged commit cb15872 into vimeo:master Feb 28, 2022
@orklah
Copy link
Collaborator

orklah commented Feb 28, 2022

Thanks! Sorry, for the delay ;)

@aurimasniekis
Copy link

@Patrick-Remy thanks for implementing this feature, but I am having some trouble trying to use it. I have tried many different combinations but I really can't seem to find a way to make it work.

P.S. I am trying on dev-master branch

Few examples:

<?php

class Test
{
    /**
     * @template T of object
     *
     * @param T                        $object
     * @param key-of<properties-of<T>> $name
     *
     * @return void
     */
    public static function check(object $object, string $name): void
    {
    }
}

class Data
{
    public string $foo = 'foo!';
    public int    $bar = 42;
}

$data = new Data();

Test::check($data, 'aa');
ERROR: InvalidDocblock - a.php:74:15 - Untemplated key-of param properties-of<T> should be an array in docblock for Test::check (see https://psalm.dev/008)
     * @param key-of<properties-of<T>> $name
<?php

class Test
{
    /**
     * @template T of object
     * @template P of properties-of<T>
     *
     * @param T         $object
     * @param key-of<P> $name
     *
     * @return void
     */
    public static function check(object $object, string $name): void
    {
    }
}

class Data
{
    public string $foo = 'foo!';
    public int    $bar = 42;
}

$data = new Data();

Test::check($data, 'foo');
Test::check($data, 'aa');
ERROR: MismatchingDocblockParamType - a.php:75:15 - Parameter $name has wrong type 'key-of<P>', should be 'string' (see https://psalm.dev/141)
     * @param key-of<P> $name


ERROR: InvalidArgument - a.php:92:20 - Argument 2 of Test::check expects key-of<P:fn-test::check as properties-of<T:fn-test::check as object>>, 'foo' provided (see https://psalm.dev/004)
Test::check($data, 'foo');


ERROR: InvalidArgument - a.php:93:20 - Argument 2 of Test::check expects key-of<P:fn-test::check as properties-of<T:fn-test::check as object>>, 'aa' provided (see https://psalm.dev/004)
Test::check($data, 'aa');

Also key-of<T> doesn't look like works and just says no issues:

<?php

class Test
{
    /**
     * @template T of array
     *
     * @param T         $input
     * @param key-of<T> $name
     *
     * @return void
     */
    public static function check(array $input, string $name): void
    {
    }
}

$data = ['foo' => 'ddd', 'bar' => 42];

Test::check($data, 'foo');
Test::check($data, 'aa');

Tried even providing array shape but no luck.

/** @var array{foo: string, bar: int} $data */

@Patrick-Remy
Copy link
Contributor Author

Thanks for your examples, these scenarios aren't currently tested, I'll have a look at these and try to figure out what is going wrong there!

@aurimasniekis
Copy link

No worries, as far as I have tried to debug, I think the main issues is that key-of doesn't try to resolve non template values, and for e.g. if value requires to pass it through resolver it doesn't do it.

@boesing
Copy link
Contributor

boesing commented Feb 7, 2023

@aurimasniekis Did you raised an issue for this specific problem? Pretty sure that it won't get fixed if not raised properly.
I can create an issue in case you don't want to create it. 🤙🏼

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
PR: Need review release:feature The PR will be included in 'Features' section of the release notes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Pseudo-types for referencing class properties and methods?
6 participants