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: make key-of/value-of usable with non-const arrays #7396

Merged
merged 11 commits into from Jan 31, 2022
Merged

feat: make key-of/value-of usable with non-const arrays #7396

merged 11 commits into from Jan 31, 2022

Conversation

Patrick-Remy
Copy link
Contributor

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

Currently key-of<T> and value-of<T> helper-types are only usable with class constants. But they can be even more powerful! This PR tries to add the following capabilites:

  1. Usage with array-like types (includes keyed array, and lists), even with literals
/**
 * @return key-of<list<string>>
 */
/**
 * @return key-of<array{a: string, b: string)>
 */
  1. Support for unions as T-param
/**
 * @return key-of<array<int, string>|array<float, string>>
 */
  1. Improve template support of key-of, so that correctly not only array-key is checked, but instead only methods like array_keys can be used to ensure template support.
/**
 * @template T of array
 * @param T $array
 * @return key-of<T>[]
 */
function getKeys($array) {
    return array_keys($array);
}
  1. Add template support to value-of which accepts array_values if used correctly.
/**
 * @template T of array
 * @param T $array
 * @return value-of<T>[]
 */
function getValues($array) {
    return array_values($array);
}

Unfortunately @psalm-assert-if-true seems to not support template types, and even if I fix a bug inside FunctionLikeDocblockScanner where template types aren't passed I have no success. So I welcome any suggestions to this! I saw that there is made use of assertions for array-key-exists but I didn't find how I can change the type of $key so that this test won't fail:

/**
 * @template T of array
 * @param T $array
 * @return key-of<T>|null
 */
function getKey(string $key, $array) {
    if (array_key_exists($key, $array)) {
        return $key;
     }
     return null;
}

@orklah orklah marked this pull request as draft January 14, 2022 18:14
@orklah
Copy link
Collaborator

orklah commented Jan 14, 2022

Converting to draft (just for easier maintenance)

@Patrick-Remy
Copy link
Contributor Author

Do you have any idea how I can achieve the described case for array_key_exists?

@orklah
Copy link
Collaborator

orklah commented Jan 15, 2022

I'm not sure I understood the issue exactly, but did you look at this:

if ($assertion === 'array-key-exists'

using array_key_exists will generate an assertion array-key-exists. It may be possible to adapt this to be able to change $key.
However, you'll probably start asking questions about this kind of cases: https://psalm.dev/r/e28860fd8a

The array here has int|string keys. You know $key is already a string. What will $key be after you array_key_exists? If you say key-of<$array>, you lost information about $key being a string. If you say string, you lost the information about $key being a valid offset. You'd need something like string-key-of<$array>, I think. But this is only a valid solution for keys, values will be different. And even for keys, if $key was 'a'|'b' instead of string, you're back to square one.

The type system is very complicated and I can only advise that you finish your first PR and all that will be related to it until you start on more

@psalm-github-bot
Copy link

I found these snippets:

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

/**
 * @template T of array<string|int, mixed>
 * @param T $array
 * @return key-of<T>|null
 */
function getKey(string $key, $array) {
    if (array_key_exists($key, $array)) {
        return $key;
     }
     return null;
}
Psalm output (using commit 1f0d3de):

No issues!

@Patrick-Remy
Copy link
Contributor Author

The array here has int|string keys. You know $key is already a string. What will $key be after you array_key_exists? If you say key-of<$array>, you lost information about $key being a string. If you say string, you lost the information about $key being a valid offset.
You're completely right, replacing the type with key-of<$array> was a bad idea.

You'd need something like string-key-of<$array>, I think. But this is only a valid solution for keys, values will be different. And even for keys, if $key was 'a'|'b' instead of string, you're back to square one.
It just would be great, if the snippet would pass with my implementation, as it's completely valid. My current approach was to extend UnionTypeComparator::isContainedBy to check if $container_type is TTemplateKeyOf and there is an assertion array-key-exists for the $input_type with a relation to the type of $container_type, it would be ok. Unfortunately in this context all assertions are gone. So I'm not sure if this is the right context.

Probably this case is something for later. I fixed all other outstanding issues, the original parsing for TTemplateKeyOf didn't initialized with the original TTemplateParam type, which led to not being replaced and was always empty.
Could you please review again?

@Patrick-Remy Patrick-Remy marked this pull request as ready for review January 16, 2022 21:55
@orklah
Copy link
Collaborator

orklah commented Jan 16, 2022

This will need to target the master branch, removing existing types is a big BC break.

Also, can you fix the errors in shepherd before we review? It's possible you'll see issues that could have a big impact on your existing code. Reviewing is kinda hard, so I'd prefer doing that once everything is fixed!

@Patrick-Remy
Copy link
Contributor Author

Okay, I will rebase to master, do you have some time schedule for the 5.x release?

I'm not quite sure, if the latest shepherd issues are related to my changes. I'll have a look tomorrow.

@orklah
Copy link
Collaborator

orklah commented Jan 16, 2022

We don't have a release date yet for Psalm 5, we'll make sure to communicate when we will

About the errors on shepherd, I'm pretty sure they're related to your changes. Upstream branch is clean and the errors seem consistent with what you changed

@Patrick-Remy Patrick-Remy changed the base branch from 4.x to master January 18, 2022 21:19
@Patrick-Remy
Copy link
Contributor Author

Patrick-Remy commented Jan 18, 2022

I think I'm finally done rebasing to master and fixing the outstanding shepherd bugs, which were caused by not using TemplateStandinTypeReplacer::getMostSpecificTypeFromBounds() to replace the template param.

All failing tests/checks are also currently failing on the master branch, if I didn't oversee something.

Ready for review now – hopefully 😅

FYI, I now no longer will force-push the branch, so that changes are transparent.

@Patrick-Remy Patrick-Remy changed the title WIP feat: make key-of/value-of usable with non-const arrays feat: make key-of/value-of usable with non-const arrays Jan 18, 2022
@orklah
Copy link
Collaborator

orklah commented Jan 18, 2022

there are errors in shepherd, they are related to a change in test format in master.

We know require tests to be all associative arrays. Can you change the format of the tests you added?

@Patrick-Remy
Copy link
Contributor Author

Patrick-Remy commented Jan 18, 2022

I already changed the format of my added tests, but there are tests in the master branch which aren't correct, see:

my shepherd run: https://github.com/vimeo/psalm/runs/4859814032?check_suite_focus=true
latest master shepherd run: https://github.com/vimeo/psalm/runs/4857096400?check_suite_focus=true

Same for the failing unit tests.

@orklah
Copy link
Collaborator

orklah commented Jan 18, 2022

sorry, this is actually my fault. I'm fixing that

@orklah
Copy link
Collaborator

orklah commented Jan 18, 2022

fixed :) Thanks for reporting, I forgot to check after the last merge

@Patrick-Remy
Copy link
Contributor Author

Thanks! I just rebased 👍

@orklah orklah added the release:feature The PR will be included in 'Features' section of the release notes label Jan 18, 2022
tests/KeyOfArrayTest.php Outdated Show resolved Hide resolved
@Patrick-Remy Patrick-Remy marked this pull request as ready for review January 27, 2022 21:38
Not in all cases the TemplateParam gets replaced before type checking,
in these cases, use the defined `as` type.
Refactor to extract key/value type of array union to method.
@Patrick-Remy
Copy link
Contributor Author

Patrick-Remy commented Jan 27, 2022

@orklah any idea how to pass this test here?

<?php
/**
  * @template TKey of int
  * @template TArray of array<TKey, bool>
  * @param TArray $array
  * @return list<TKey>
  */
function toListOfKeys(array $array): array {
    return array_keys($array);
}

I have no idea why, but while debugging TArray has key-type array-key instead of expected int (or TTemplateParam TKey).

@Patrick-Remy
Copy link
Contributor Author

@Patrick-Remy Don't know if you care to, but you might be able to add support for stuff like this without too much trouble.

@AndrolGenhald sorry, this PR has already been 'exploded' a bit and took a lot of time, should be handled in a separate PR after this has been merged.

@psalm-github-bot
Copy link

I found these snippets:

https://psalm.dev/r/3531bf3469
<?php

class Foo
{
    /** @var non-empty-list<int> */
    public const BAZ = [1];

    /** @return value-of<static::BAZ> */
    public static function foobar(int $int): int
    {
        return static::BAZ[0];
    }
}
Psalm output (using commit 9168cef):

ERROR: UnresolvableConstant - 8:17 - Could not resolve constant static::BAZ

ERROR: MismatchingDocblockReturnType - 8:17 - Docblock has incorrect return type 'value-of<static::BAZ>', should be 'int'

INFO: MixedInferredReturnType - 8:17 - Could not verify return type 'value-of<static::BAZ>' for Foo::foobar

@orklah
Copy link
Collaborator

orklah commented Jan 28, 2022

@orklah any idea how to pass this test here?

Not sure what you mean, the snippet https://psalm.dev/r/d16778139e seems to work and return list<1> on your branch as I'd expect

@psalm-github-bot
Copy link

I found these snippets:

https://psalm.dev/r/d16778139e
<?php
/**
  * @template TKey of int
  * @template TArray of array<TKey, bool>
  * @param TArray $array
  * @return list<TKey>
  */
function toListOfKeys(array $array): array {
    return array_keys($array);
}

$a = toListOfKeys([1 => 'a']);

/** @psalm-trace $a */;
Psalm output (using commit 8914793):

ERROR: InvalidArgument - 12:19 - Argument 1 of toListOfKeys expects array<int, bool>, array{1: 'a'} provided

INFO: Trace - 14:23 - $a: list<1>

INFO: UnusedVariable - 12:1 - $a is never referenced or the value is not used

@Patrick-Remy
Copy link
Contributor Author

@orklah any idea how to pass this test here?

Not sure what you mean, the snippet https://psalm.dev/r/d16778139e seems to work and return list<1> on your branch as I'd expect

Are you sure you ran it on my branch (commit 51cab704c4626b8ee55472d82542ea4b031b8960)? I get:

1) Psalm\Tests\KeyOfTemplateTest::testValidCode with data set "keyOfNestedTemplates" ('<?php\n                /**\n ...$a */;')
Psalm\Exception\CodeException: MixedReturnTypeCoercion - src/somefile.php:9:27 - The type 'list<key-of<TArray:fn-array_keys as TArray:fn-array_keys as array<array-key, mixed>>>' is more general than the declared return type 'list<TKey:fn-tolistofkeys as int>' for toListOfKeys

It's also already added as a test in tests/Template/KeyOfTemplateTest.php as SKIPPED-.

@psalm-github-bot

This comment has been minimized.

@Patrick-Remy
Copy link
Contributor Author

I got it, I didn't implement replaceWithTemplateArgs and I didn't add a comparison for TTemplateKeyOf also in AtomicTypeComparator before TTemplateParam. Need to refactor a bit tomorrow and then it will be ready for review!

Extend TTemplateKeyOf from Atomic instead of Scalar, to ensure it is
only compared in AtomicTypeComparator to reduce duplicate code.
@Patrick-Remy
Copy link
Contributor Author

@orklah It's finally done – could you review again please?

@orklah
Copy link
Collaborator

orklah commented Jan 31, 2022

I'm ready to merge, no last change? :)

@Patrick-Remy
Copy link
Contributor Author

Patrick-Remy commented Jan 31, 2022

Needs anything to be added to the UPGRADING.md file?

@orklah
Copy link
Collaborator

orklah commented Jan 31, 2022

Yeah, maybe document the change of inheritance

@Patrick-Remy
Copy link
Contributor Author

I also added the renames as BCs.

@orklah orklah merged commit 2e01e9b into vimeo:master Jan 31, 2022
@orklah
Copy link
Collaborator

orklah commented Jan 31, 2022

Thanks!

@Patrick-Remy
Copy link
Contributor Author

Many thanks for your intensive and always fast review! 😊

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
PR: Needs work 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.

None yet

3 participants