-
Notifications
You must be signed in to change notification settings - Fork 504
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
Feature check array functions which require stringish values #3132
Feature check array functions which require stringish values #3132
Conversation
array_combine([true, null, 3.14], ['a', 'b', 'c']); | ||
array_fill_keys([true, null, 3.14], 'a'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be allowed? IMO yes (it could be prohibited by more opinionated rulesets - e.g. strict-rules). However, currently it is a bit inconsistent. See https://phpstan.org/r/266e4f9e-a4c9-4679-ba6f-299684a10f7e
array_fill_keys
is allowed.array_combine
is allowed with PHP < 8, and prohibited afterwards. That looks like an oversight, because as far as I can tell the function behaves the same: https://3v4l.org/dvTNC
I checked the integration test errors (https://github.com/phpstan/phpstan-src/actions/runs/9435637126/job/25989364652?pr=3132):
The |
This pull request has been marked as ready for review. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For backward compatibility, I'd like this approach:
- Keep ImplodeFunctionRule
- Deprecate ImplodeFunctionRule and disable it in bleeding edge (like NoopRule is)
- Introduce new rule like you did, but enable it only in bleeding edge
true, | ||
) | ||
) { | ||
$argsToCheck = [0 => $args[0]]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the reasoning behind this condition?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure what you're asking about exactly. These are functions which either only have one argument, or which have multiple arguments and only the first one has to be array of values castable to string.
if (count($args) === 1) { | ||
$argsToCheck = [0 => $args[0]]; | ||
} elseif (count($args) === 2) { | ||
$argsToCheck = [1 => $args[1]]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know you probably just copied this from ImplodeFunctionRule, but the assumption is wrong because of named arguments. Someone might call implode(array: $a, separator: $s)
. and this code would break.
Please write a test for this scenario. The scenario can be fixed by processing $args
through ArgumentsNormalizer
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch - I didn't think of that, since I just modifed the ImplodeFunctionRule. There was also a similar issue when no arguments were passed etc.
However, it lead me down another rabbit hole: when I added support for named arguments, I had to do something with the Parameter #1 of function ...
, since the number might now be incorrect. So I attempted to make it consistent with how FunctionCallParametersCheck
names the parameters. But it feels like individual rules shouldn't deal with that at all, since then it will "never" be consistent. If this is already handled somehow, maybe you can point me in the right direction.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, other rules don't deal with this at all now. We could refactor FunctionCallParametersCheck to be more flexible and to use its internals in other rules, but I don't think it's necessary in this PR.
Another thing this would allow us is to deal with $arg–>unpack
properly same way FunctionCallParametersCheck does - it actually looks into $type->getConstantArrays()
and expands them:
phpstan-src/src/Rules/FunctionCallParametersCheck.php
Lines 114 to 162 in 38f3906
if ($arg->unpack) { | |
$arrays = $type->getConstantArrays(); | |
if (count($arrays) > 0) { | |
$minKeys = null; | |
foreach ($arrays as $array) { | |
$countType = $array->getArraySize(); | |
if ($countType instanceof ConstantIntegerType) { | |
$keysCount = $countType->getValue(); | |
} elseif ($countType instanceof IntegerRangeType) { | |
$keysCount = $countType->getMin(); | |
if ($keysCount === null) { | |
throw new ShouldNotHappenException(); | |
} | |
} else { | |
throw new ShouldNotHappenException(); | |
} | |
if ($minKeys !== null && $keysCount >= $minKeys) { | |
continue; | |
} | |
$minKeys = $keysCount; | |
} | |
for ($j = 0; $j < $minKeys; $j++) { | |
$types = []; | |
$commonKey = null; | |
foreach ($arrays as $constantArray) { | |
$types[] = $constantArray->getValueTypes()[$j]; | |
$keyType = $constantArray->getKeyTypes()[$j]; | |
if ($commonKey === null) { | |
$commonKey = $keyType->getValue(); | |
} elseif ($commonKey !== $keyType->getValue()) { | |
$commonKey = false; | |
} | |
} | |
$keyArgumentName = null; | |
if (is_string($commonKey)) { | |
$keyArgumentName = $commonKey; | |
$hasNamedArguments = true; | |
} | |
$arguments[] = [ | |
$arg->value, | |
TypeCombinator::union(...$types), | |
false, | |
$keyArgumentName, | |
$arg->getStartLine(), | |
]; | |
} | |
} else { |
if ($normalizedArgs === []) { | ||
return []; | ||
} | ||
$argsToCheck = [0 => $normalizedArgs[0]]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if the index does not exist?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't think of that. But it seems that this cannot happen on non-empty $normalizedArgs
, because the normalizer only fills optional parameter holes, if a required parameter is missing it seems to return null. I guess it doesn't hurt to be extra defensive and check it anyway.
continue; | ||
} | ||
|
||
$origNamedArgs[$arg->name->toString()] = $arg; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You don't need to do this, on normalized arg they're available in an attribute.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't notice that. But due to the usage of $origNamedArgs
for handling implode (I probably added that after you wrote the comment), I still need to have a map of named args.
07ea6aa
to
58c4447
Compare
Thank you! |
This is an attempt to detect values which are not castable to string in functions which expect an array of values castable to string (e.g.
array_unique([new stdClass(), new stdClass()])
).I considered using
__stringandstringable
in functionMap, but then I saw this comment: #3110 (comment) . Fortunately, I found thatImplodeFunctionRule
already does the same thing, so I just generalized it to work with all the relevant functions.