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

#4997 added more precise type inference for count() returning 0 or positive-int on known arrays #4999

Merged

Conversation

Ocramius
Copy link
Contributor

@Ocramius Ocramius commented Jan 13, 2021

Fixes #4997

Note: I only provided the stub, but am unsure about where tests for stubs (if there are any) are applicable.

Questions open:

@orklah
Copy link
Collaborator

orklah commented Jan 13, 2021

Seems like current CallMap expect Countable|array|SimpleXMLElement|ResourceBundle as parameters. SimpleXMLElement is not countable nor array but can be counted through voodoo. Not sure about ResourceBundle though...

@orklah
Copy link
Collaborator

orklah commented Jan 13, 2021

@psalm-flow is related to the continuity of taint though the function. It shouldn't be needed for count.

…ourceBundle` counting, as well as handling hardcoded 2-element-arrays cases

This patch:

 * adds support for `count(\SimpleXmlElement)` (https://www.php.net/manual/en/simplexmlelement.count.php)
 * adds support for `count(\ResourceBundle)` (https://www.php.net/manual/en/resourcebundle.count.php)
 * removes usage of global constants from stub (not supported - see https://www.php.net/manual/en/function.count.php)
 * adds support for identifying fixed-element-count arrays, for example `count(callable&array)`, which is always `2`
tests/FunctionCallTest.php Outdated Show resolved Hide resolved
@azjezz
Copy link
Contributor

azjezz commented Jan 13, 2021

@psalm-pure - unclear whether this is to be declared. Purity depends on the given argument: perhaps @azjezz knows more here?

we can't really declare count pure as it depends on the argument type ( #4145 ) ( unless we can do function overrides in stubs )

If $value is array, SimpleXMLElement, or ResourceBundle, then count is pure.
If $value is Countable and ( $value is immutable or count(): int is declared with @psalm-mutation-free ), then count is pure
otherwise, count is not pure.

@Ocramius
Copy link
Contributor Author

Ocramius commented Jan 13, 2021

EDIT: solved ✔️ (no need to refactor)

This block can probably also be refactored away somehow:

case 'count':
if (($first_arg_type = $statements_analyzer->node_data->getType($call_args[0]->value))) {
$atomic_types = $first_arg_type->getAtomicTypes();
if (count($atomic_types) === 1) {
if (isset($atomic_types['array'])) {
if ($atomic_types['array'] instanceof Type\Atomic\TCallableArray
|| $atomic_types['array'] instanceof Type\Atomic\TCallableList
|| $atomic_types['array'] instanceof Type\Atomic\TCallableKeyedArray
) {
return Type::getInt(false, 2);
}
if ($atomic_types['array'] instanceof Type\Atomic\TNonEmptyArray) {
return new Type\Union([
$atomic_types['array']->count !== null
? new Type\Atomic\TLiteralInt($atomic_types['array']->count)
: new Type\Atomic\TInt
]);
}
if ($atomic_types['array'] instanceof Type\Atomic\TNonEmptyList) {
return new Type\Union([
$atomic_types['array']->count !== null
? new Type\Atomic\TLiteralInt($atomic_types['array']->count)
: new Type\Atomic\TInt
]);
}
if ($atomic_types['array'] instanceof Type\Atomic\TKeyedArray
&& $atomic_types['array']->sealed
) {
return new Type\Union([
new Type\Atomic\TLiteralInt(count($atomic_types['array']->properties))
]);
}
}
}
}
break;

The new stubs don't seem to contradict it, but I'm not sure how well tested it is right now, nor if the stubs override it.

@Ocramius
Copy link
Contributor Author

Ocramius commented Jan 13, 2021

EDIT: solved ✔️

@orklah ResourceBundle was indeed added to the signature, but downstream it fails in real-world projects:

ERROR: UndefinedClass - src/Framework/MockObject/Rule/ConsecutiveParameters.php:117:18 - Class or interface ResourceBundle does not exist (see https://psalm.dev/019)
        foreach ($parameters as $i => $parameter) {

https://app.circleci.com/pipelines/github/vimeo/psalm/4824/workflows/6f0caadd-ba64-414d-ad4e-d453e9cc6dab/jobs/21060, git@github.com:muglug/phpunit.git

Perhaps I should allow object instead? Or perhaps we should really just stop allowing SimpleXMLElement and ResourceBundle :X

@azjezz
Copy link
Contributor

azjezz commented Jan 13, 2021

we should really just stop allowing SimpleXMLElement and ResourceBundle :X

👍 for that

@Ocramius
Copy link
Contributor Author

Ocramius commented Jan 13, 2021

EDIT: solved ✔️

Mutation modifier seems to be computed in psalm's internals:

if ($function_id === 'count' && isset($args[0]) && $type_provider) {
$count_type = $type_provider->getType($args[0]->value);
if ($count_type) {
foreach ($count_type->getAtomicTypes() as $atomic_count_type) {
if ($atomic_count_type instanceof TNamedObject) {
$count_method_id = new MethodIdentifier(
$atomic_count_type->value,
'count'
);
try {
$method_storage = $codebase->methods->getStorage($count_method_id);
return $method_storage->mutation_free;
} catch (\Exception $e) {
// do nothing
}
}
}
}
}

I think we're OK as-is, if that is still taken into account whilst looking at the stubs.

I'd gladly write test scenarios for these cases, just need someone to point me at an appropriate location where to put them :-)

…eInt` for `count(TNonEmptyArray)` and `count(TNonEmptyList)`
@orklah
Copy link
Collaborator

orklah commented Jan 13, 2021

I believe FunctionCallTest.php is the appropriate location for those. I added tests here in the past (for example for parse_url)

@Ocramius Ocramius changed the title #4997 added more precise stub for count() returning 0 or positive-int on known types #4997 added more precise type inference for count() returning 0 or positive-int on known arrays Jan 13, 2021
…recise type of a `\count(T)`

expression when given a `T`, so we baked the whole type resolution for `positive-int`, `0` and
`positive-int|0` directly in there.

While this complicates things, it is also true that it is not possible right now (for the stubs)
to provide the level of detail around `count()` that is required by the type inference system
for such a complex function with so many different semantics.
@Ocramius Ocramius force-pushed the feature/#4997-more-precise-count-return-type-stub branch from 3758952 to fad0042 Compare January 13, 2021 14:10
@Ocramius
Copy link
Contributor Author

@orklah thanks for pushing me in the right direction: I've added detailed tests around positive-int and 0 return types of count(), as well as its purity properties.

This ended up being much simpler than expected, given that the pre-existing code needed very minor adjustments to operate as expected, so we should be good to review and hopefully merge if the solution is acceptable 👍

@Ocramius Ocramius marked this pull request as ready for review January 13, 2021 14:16
@muglug muglug merged commit a53cc23 into vimeo:master Jan 13, 2021
@muglug
Copy link
Collaborator

muglug commented Jan 13, 2021

Thanks!

@Ocramius Ocramius deleted the feature/#4997-more-precise-count-return-type-stub branch January 13, 2021 19:40
danog pushed a commit to danog/psalm that referenced this pull request Jan 29, 2021
…`0` or `positive-int` on known arrays (vimeo#4999)

* vimeo#4997 added more precise stub for `count()` returning `0` or `positive-int` on known types

* vimeo#4997 updated `count()` to support `\SimpleXmlElement` and `\ResourceBundle` counting, as well as handling hardcoded 2-element-arrays cases

This patch:

 * adds support for `count(\SimpleXmlElement)` (https://www.php.net/manual/en/simplexmlelement.count.php)
 * adds support for `count(\ResourceBundle)` (https://www.php.net/manual/en/resourcebundle.count.php)
 * removes usage of global constants from stub (not supported - see https://www.php.net/manual/en/function.count.php)
 * adds support for identifying fixed-element-count arrays, for example `count(callable&array)`, which is always `2`

* vimeo#4997 adapted `FunctionCallReturnTypeFetcher` to infer `TPositiveInt` for `count(TNonEmptyArray)` and `count(TNonEmptyList)`

* The `FunctionCallReturnTypeFetcher` is responsible for defining the precise type of a `\count(T)`
expression when given a `T`, so we baked the whole type resolution for `positive-int`, `0` and
`positive-int|0` directly in there.

While this complicates things, it is also true that it is not possible right now (for the stubs)
to provide the level of detail around `count()` that is required by the type inference system
for such a complex function with so many different semantics.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

count(array) should be positive-int|0, while count(Countable) should be int
4 participants