Skip to content

Limit int ranges when narrowing arrays via count() #3902

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

Merged
merged 2 commits into from
Mar 27, 2025

Conversation

herndlm
Copy link
Contributor

@herndlm herndlm commented Mar 24, 2025

Fixes phpstan/phpstan#12787

for const ints a limit existed already.

@herndlm herndlm force-pushed the fix-12787 branch 2 times, most recently from 581ec91 to 29d5146 Compare March 24, 2025 21:26
@herndlm herndlm marked this pull request as draft March 25, 2025 10:52
@herndlm herndlm marked this pull request as ready for review March 25, 2025 11:17
@phpstan-bot
Copy link
Collaborator

This pull request has been marked as ready for review.

&& $sizeType->getMin() <= ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT
&& ($sizeType->getMax() === null || $sizeType->getMax() - $sizeType->getMin() <= ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT)
&& $arrayType->getKeyType()->isSuperTypeOf(IntegerRangeType::fromInterval(0, $sizeType->getMin() - 1))->yes()
&& ($sizeType->getMax() === null || $arrayType->getKeyType()->isSuperTypeOf(IntegerRangeType::fromInterval(0, $sizeType->getMax() - 1))->yes())
Copy link
Member

Choose a reason for hiding this comment

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

This isn't what I'd expect. You should first calculate the expected array size and then compare it with ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT.

What you're doing is that you're first comparing min with the limit, and then (max - min) with the limit. Theoretically you can end up with a much larger array than the limit.

Thanks.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

the code here that creates the arrays, is a bit complex and dynamic. and it was like that already before I did my changes. but I'll try to see what I can do

Copy link
Member

Choose a reason for hiding this comment

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

I just want to count how many time exactly is setOffsetValueType being called, and that number has to be under the limit.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

sure, I understand. I checked again and slightly changed things. the max - min was not useful, I agree. I think the current state is OK, but let me know please.

what the code for int ranges basically does is 3 things

  1. use the min value to create non-optional offsets in a constant array. this is what makes it create a huuge constant array in the reproducer. and min decides how often setOffsetValueType() is being called.
  2. use the max value to then continue starting from min up until max to create optional offsets. here we get then up until max setOffsetValueType() calls in summary. so this should also be under the limit.
  3. use an existing constant array, start with min, building a list until it finds an offset that does not exist. this is a bit weird for me still, but at least not relevant to limits I'd say, since it does not create new offsets

what do you think?

Copy link
Contributor Author

@herndlm herndlm Mar 25, 2025

Choose a reason for hiding this comment

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

summary: min and max should be under the limit IMO. checking max would be enough if it is set, but the if is already complex enough, so currently both are checked

Copy link
Contributor Author

Choose a reason for hiding this comment

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

writing this down made me realize it can be a bit simpler. pushed again 🙈

@herndlm herndlm requested a review from ondrejmirtes March 25, 2025 19:30
$isList->yes()
|| $isConstantArray->yes() && $arrayType->getKeyType()->isSuperTypeOf(IntegerRangeType::fromInterval(0, $sizeType->getMin() - 1))->yes()
)
&& ($sizeType->getMax() ?? $sizeType->getMin()) < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT
Copy link
Member

Choose a reason for hiding this comment

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

I don't think that ($sizeType->getMax() ?? $sizeType->getMin()) is the final size of the array created here.

Please refactor this section. You can prepare the arguments for setOffsetValueType in a single array beforehand, count the number of elements, and then proceed with creating the constant array only if it's below ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT.

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 still think it does, I tried to describe the logic here #3902 (comment)

but you can also check

// turn optional offsets non-optional
$valueTypesBuilder = ConstantArrayTypeBuilder::createEmpty();
for ($i = 0; $i < $sizeType->getMin(); $i++) {
$offsetType = new ConstantIntegerType($i);
$valueTypesBuilder->setOffsetValueType($offsetType, $arrayType->getOffsetValueType($offsetType));
}
if ($sizeType->getMax() !== null) {
for ($i = $sizeType->getMin(); $i < $sizeType->getMax(); $i++) {
$offsetType = new ConstantIntegerType($i);
$valueTypesBuilder->setOffsetValueType($offsetType, $arrayType->getOffsetValueType($offsetType), true);
}
} elseif ($arrayType->isConstantArray()->yes()) {
for ($i = $sizeType->getMin();; $i++) {
$offsetType = new ConstantIntegerType($i);
$hasOffset = $arrayType->hasOffsetValueType($offsetType);
if ($hasOffset->no()) {
break;
}
$valueTypesBuilder->setOffsetValueType($offsetType, $arrayType->getOffsetValueType($offsetType), !$hasOffset->yes());
}
} else {
$resultTypes[] = TypeCombinator::intersect($arrayType, new NonEmptyArrayType());
continue;
}
$resultTypes[] = $valueTypesBuilder->getArray();

case (for loop) 1 and 2 combined call setOffsetValueType() up to max times. case 3 is special but limited by an existing constant array which should make it safe.

do you disagree? I don't even know how to refactor this right now tbh, I think the "max if set, otherwise min" is the simplest way of dealing with this.

@ondrejmirtes
Copy link
Member

Check the 2nd commit - this is what I meant.

@ondrejmirtes ondrejmirtes merged commit c4cc640 into phpstan:2.1.x Mar 27, 2025
101 of 105 checks passed
@ondrejmirtes
Copy link
Member

Thank you.

@herndlm
Copy link
Contributor Author

herndlm commented Mar 27, 2025

Check the 2nd commit - this is what I meant.

Oh. Sorry for not getting you and thx for adapting it yourself. Will check it later in detail.

@herndlm herndlm deleted the fix-12787 branch March 27, 2025 16:06
@herndlm
Copy link
Contributor Author

herndlm commented Mar 28, 2025

Tbh, I still think my solution is enough and was simpler of course. But yes, yours is slightly safer I'd say 😊

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.

Performance regression involving array count check
4 participants