-
Notifications
You must be signed in to change notification settings - Fork 574
Invalidate maybe-impure function return values after impure method/static calls #5667
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
base: 2.1.x
Are you sure you want to change the base?
Changes from 9 commits
e9ba032
923d2b1
e731e88
452a3c8
7687df3
1fd7cb8
968e01a
aadf81a
9d80f7d
5ef8c3d
159e29a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,6 +26,7 @@ | |
| use PHPStan\Type\ArrayType; | ||
| use PHPStan\Type\ClassStringType; | ||
| use PHPStan\Type\Constant\ConstantBooleanType; | ||
| use PHPStan\Type\Constant\ConstantIntegerType; | ||
| use PHPStan\Type\FloatType; | ||
| use PHPStan\Type\Generic\GenericClassStringType; | ||
| use PHPStan\Type\IntegerType; | ||
|
|
@@ -1354,6 +1355,22 @@ private static function createInstanceOf(string $className, string $variableName | |
| return new Expr\Instanceof_(new Variable($variableName), new Name($className)); | ||
| } | ||
|
|
||
| public function testUnknownFunctionSubExpressionDoesNotPreventNarrowing(): void | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could this be tested via StrContainingTypeSpecifyingExtension with a str-containing function instead?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. Replaced the contrived
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the newly added test does not fail when reverting aadf81a
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pushed. The fix restores the |
||
| { | ||
| $fauxFuncCall = new FuncCall(new Name('FAUX_FUNCTION'), [new Arg(new Variable('foo'))]); | ||
| $countCall = new FuncCall(new Name('count'), [new Arg($fauxFuncCall)]); | ||
|
|
||
| $specifiedTypes = $this->typeSpecifier->create( | ||
| $countCall, | ||
| new ConstantIntegerType(1), | ||
| TypeSpecifierContext::createTrue(), | ||
| $this->scope, | ||
| ); | ||
|
|
||
| $result = $this->toReadableResult($specifiedTypes); | ||
| $this->assertSame(['count(FAUX_FUNCTION($foo))' => '1'], $result); | ||
| } | ||
|
|
||
| /** | ||
| * @param non-empty-string $functionName | ||
| */ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types = 1); | ||
|
|
||
| namespace Bug13416; | ||
|
|
||
| use function PHPStan\Testing\assertType; | ||
|
|
||
| class MyRecord | ||
| { | ||
| /** @var list<self> */ | ||
| private static array $storage = []; | ||
|
|
||
| /** | ||
| * @return list<self> | ||
| * @phpstan-impure | ||
| */ | ||
| public static function find(): array | ||
| { | ||
| return self::$storage; | ||
| } | ||
|
|
||
| /** @phpstan-impure */ | ||
| public function insert(): void | ||
| { | ||
| self::$storage[] = $this; | ||
| } | ||
|
|
||
| /** | ||
| * @return non-empty-string | ||
| * @phpstan-impure | ||
| */ | ||
| public function getName(): string | ||
| { | ||
| return 'test'; | ||
| } | ||
| } | ||
|
|
||
| class Repository | ||
| { | ||
| /** | ||
| * @return list<MyRecord> | ||
| * @phpstan-impure | ||
| */ | ||
| public function findAll(): array | ||
| { | ||
| return []; | ||
| } | ||
|
|
||
| /** @phpstan-impure */ | ||
| public function save(MyRecord $record): void | ||
| { | ||
| } | ||
| } | ||
|
|
||
| function testImpureStaticCallNotNarrowedByCount(): void | ||
| { | ||
| assert(count(MyRecord::find()) === 1); | ||
| // Impure call result should not be narrowed | ||
| assertType('int<0, max>', count(MyRecord::find())); | ||
| } | ||
|
|
||
| function testImpureMethodCallNotNarrowedByCount(): void | ||
| { | ||
| $repo = new Repository(); | ||
|
|
||
| assert(count($repo->findAll()) === 1); | ||
| // Impure call result should not be narrowed | ||
| assertType('int<0, max>', count($repo->findAll())); | ||
| } | ||
|
|
||
| function testStrlenOfImpureCallNotNarrowed(): void | ||
| { | ||
| $record = new MyRecord(); | ||
|
|
||
| assert(strlen($record->getName()) === 3); | ||
| // strlen wrapping an impure call should not be narrowed | ||
| assertType('int<1, max>', strlen($record->getName())); | ||
| } | ||
|
|
||
| function testPureFunctionStaysNarrowed(): void | ||
| { | ||
| /** @var list<int> $arr */ | ||
| $arr = [1]; | ||
| assert(count($arr) === 1); | ||
| assertType('1', count($arr)); | ||
|
|
||
| $x = rand(0, 10); | ||
|
|
||
| // Pure expressions stay narrowed | ||
| assertType('1', count($arr)); | ||
| } | ||
|
|
||
| function testImpureArrowFunctionIIFE(): void | ||
| { | ||
| assert(count((fn() => MyRecord::find())()) === 1); | ||
| assertType('int<0, max>', count((fn() => MyRecord::find())())); | ||
| } | ||
|
|
||
| function testImpureClosureIIFE(): void | ||
| { | ||
| assert(count((function() { return MyRecord::find(); })()) === 1); | ||
| assertType('int<0, max>', count((function() { return MyRecord::find(); })())); | ||
| } | ||
|
|
||
| function testStrlenOfImpureArrowFunctionIIFE(): void | ||
| { | ||
| $record = new MyRecord(); | ||
| assert(strlen((fn() => $record->getName())()) === 3); | ||
| assertType('int<1, max>', strlen((fn() => $record->getName())())); | ||
| } | ||
|
|
||
| function testImpureClosureViaVariable(): void | ||
| { | ||
| $fn = function(): array { return MyRecord::find(); }; | ||
| assert(count($fn()) === 1); | ||
| assertType('int<0, max>', count($fn())); | ||
| } | ||
|
|
||
| function testImpureClosureWithEchoIIFE(): void | ||
| { | ||
| assert(strlen((function() { echo 'side-effect'; return MyRecord::find()[0]->getName(); })()) === 5); | ||
| assertType('int<1, max>', strlen((function() { echo 'side-effect'; return MyRecord::find()[0]->getName(); })())); | ||
| } | ||
|
|
||
| function testPureClosureIIFEStaysNarrowed(): void | ||
| { | ||
| /** @var list<int> $arr */ | ||
| $arr = [1, 2, 3]; | ||
| assert(count((fn() => $arr)()) === 3); | ||
| assertType('3', count((fn() => $arr)())); | ||
| } | ||
|
|
||
| /** | ||
| * @param string|null $val | ||
| * @phpstan-impure | ||
| */ | ||
| function impureFunction(?string $val): ?string | ||
| { | ||
| return $val; | ||
| } | ||
|
|
||
| function testPureOfImpureNotNarrowedByCoalesce(): void | ||
| { | ||
| $a = strlen(impureFunction('hello') ?? '') > 0; | ||
| assertType('bool', strlen(impureFunction('hello') ?? '') > 0); | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.
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.
do we have the same bug on impure
ArrowFunctions and impureClosures?if so, add tests and expand the callable to cover it
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.
Pushed. Here's a summary of the changes:
Investigation: The reviewer asked whether impure
ArrowFunctions andClosures are handled byexpressionContainsNonPureCall. The answer is yes, the current code handles them correctly through two mechanisms:(fn() => MyRecord::find())())ClosureType::isPure()viagetCallableParametersAcceptors(), which catches impurity from non-call sources likeechoTests added (7 new test cases):
strlenwrapping impure arrow function IIFEecho+ impure calls in bodyAll 12071 tests pass and PHPStan self-analysis reports no errors.