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

Feature check array functions which require stringish values #3132

Conversation

schlndh
Copy link
Contributor

@schlndh schlndh commented Jun 9, 2024

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 that ImplodeFunctionRule already does the same thing, so I just generalized it to work with all the relevant functions.

Comment on lines +59 to +80
array_combine([true, null, 3.14], ['a', 'b', 'c']);
array_fill_keys([true, null, 3.14], 'a');
Copy link
Contributor Author

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

@schlndh
Copy link
Contributor Author

schlndh commented Jun 9, 2024

I checked the integration test errors (https://github.com/phpstan/phpstan-src/actions/runs/9435637126/job/25989364652?pr=3132):

  • rector-src: AttributeGroup (1st error) and ComplexType (2nd error) don't have __toString().
  • showare: PermissionStruct doesn't have __toString().
  • The rest seem obvious.

The AnalyserIntegrationTest::testBug11147 also fails in 1.11.x, so that's unrelated.

@schlndh schlndh marked this pull request as ready for review June 9, 2024 10:51
@phpstan-bot
Copy link
Collaborator

This pull request has been marked as ready for review.

Copy link
Member

@ondrejmirtes ondrejmirtes left a 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:

  1. Keep ImplodeFunctionRule
  2. Deprecate ImplodeFunctionRule and disable it in bleeding edge (like NoopRule is)
  3. Introduce new rule like you did, but enable it only in bleeding edge

true,
)
) {
$argsToCheck = [0 => $args[0]];
Copy link
Member

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?

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'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]];
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

@ondrejmirtes ondrejmirtes Jun 9, 2024

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:

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]];
Copy link
Member

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?

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 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;
Copy link
Member

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.

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 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.

@ondrejmirtes ondrejmirtes force-pushed the feature-checkArrayFunctionsWhichRequireStringishValues branch from 07ea6aa to 58c4447 Compare June 10, 2024 08:12
@ondrejmirtes
Copy link
Member

Thank you!

@schlndh schlndh deleted the feature-checkArrayFunctionsWhichRequireStringishValues branch June 10, 2024 14:22
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.

None yet

4 participants