Skip to content

Commit 1b4b134

Browse files
authored
Transform void return to null after call
1 parent fb76c9f commit 1b4b134

File tree

12 files changed

+100
-15
lines changed

12 files changed

+100
-15
lines changed

src/Analyser/MutatingScope.php

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,8 @@ class MutatingScope implements Scope
150150

151151
private const BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH = 4;
152152

153+
private const KEEP_VOID_ATTRIBUTE_NAME = 'keepVoid';
154+
153155
/** @var Type[] */
154156
private array $resolvedTypes = [];
155157

@@ -676,6 +678,10 @@ private function getNodeKey(Expr $node): string
676678
$key .= '/*' . $node->getAttribute('startFilePos') . '*/';
677679
}
678680

681+
if ($node->getAttribute(self::KEEP_VOID_ATTRIBUTE_NAME) === true) {
682+
$key .= '/*' . self::KEEP_VOID_ATTRIBUTE_NAME . '*/';
683+
}
684+
679685
return $key;
680686
}
681687

@@ -1236,7 +1242,7 @@ private function resolveType(string $exprString, Expr $node): Type
12361242
new VoidType(),
12371243
]);
12381244
} else {
1239-
$returnType = $arrowScope->getType($node->expr);
1245+
$returnType = $arrowScope->getKeepVoidType($node->expr);
12401246
if ($node->returnType !== null) {
12411247
$returnType = TypehintHelper::decideType($this->getFunctionType($node->returnType, false, false), $returnType);
12421248
}
@@ -1492,6 +1498,9 @@ private function resolveType(string $exprString, Expr $node): Type
14921498
$matchScope = $this;
14931499
foreach ($node->arms as $arm) {
14941500
if ($arm->conds === null) {
1501+
if ($node->hasAttribute(self::KEEP_VOID_ATTRIBUTE_NAME)) {
1502+
$arm->body->setAttribute(self::KEEP_VOID_ATTRIBUTE_NAME, $node->getAttribute(self::KEEP_VOID_ATTRIBUTE_NAME));
1503+
}
14951504
$types[] = $matchScope->getType($arm->body);
14961505
continue;
14971506
}
@@ -1521,6 +1530,9 @@ private function resolveType(string $exprString, Expr $node): Type
15211530

15221531
if (!$filteringExprType->isFalse()->yes()) {
15231532
$truthyScope = $matchScope->filterByTruthyValue($filteringExpr);
1533+
if ($node->hasAttribute(self::KEEP_VOID_ATTRIBUTE_NAME)) {
1534+
$arm->body->setAttribute(self::KEEP_VOID_ATTRIBUTE_NAME, $node->getAttribute(self::KEEP_VOID_ATTRIBUTE_NAME));
1535+
}
15241536
$types[] = $truthyScope->getType($arm->body);
15251537
}
15261538

@@ -1946,7 +1958,7 @@ private function resolveType(string $exprString, Expr $node): Type
19461958
}
19471959
}
19481960

1949-
return $parametersAcceptor->getReturnType();
1961+
return $this->transformVoidToNull($parametersAcceptor->getReturnType(), $node);
19501962
}
19511963

19521964
return new MixedType();
@@ -1986,6 +1998,25 @@ private function getNullsafeShortCircuitingType(Expr $expr, Type $type): Type
19861998
return $type;
19871999
}
19882000

2001+
private function transformVoidToNull(Type $type, Node $node): Type
2002+
{
2003+
if ($node->getAttribute(self::KEEP_VOID_ATTRIBUTE_NAME) === true) {
2004+
return $type;
2005+
}
2006+
2007+
return TypeTraverser::map($type, static function (Type $type, callable $traverse): Type {
2008+
if ($type instanceof UnionType || $type instanceof IntersectionType) {
2009+
return $traverse($type);
2010+
}
2011+
2012+
if ($type->isVoid()->yes()) {
2013+
return new NullType();
2014+
}
2015+
2016+
return $type;
2017+
});
2018+
}
2019+
19892020
/**
19902021
* @param callable(Type): ?bool $typeCallback
19912022
*/
@@ -2173,6 +2204,14 @@ public function getNativeType(Expr $expr): Type
21732204
return $this->promoteNativeTypes()->getType($expr);
21742205
}
21752206

2207+
public function getKeepVoidType(Expr $node): Type
2208+
{
2209+
$clonedNode = clone $node;
2210+
$clonedNode->setAttribute(self::KEEP_VOID_ATTRIBUTE_NAME, true);
2211+
2212+
return $this->getType($clonedNode);
2213+
}
2214+
21762215
/**
21772216
* @api
21782217
* @deprecated Use getNativeType()
@@ -5103,7 +5142,7 @@ private function methodCallReturnType(Type $typeWithMethod, string $methodName,
51035142
$normalizedMethodCall = ArgumentsNormalizer::reorderStaticCallArguments($parametersAcceptor, $methodCall);
51045143
}
51055144
if ($normalizedMethodCall === null) {
5106-
return $parametersAcceptor->getReturnType();
5145+
return $this->transformVoidToNull($parametersAcceptor->getReturnType(), $methodCall);
51075146
}
51085147

51095148
$resolvedTypes = [];
@@ -5142,10 +5181,10 @@ private function methodCallReturnType(Type $typeWithMethod, string $methodName,
51425181
}
51435182

51445183
if (count($resolvedTypes) > 0) {
5145-
return TypeCombinator::union(...$resolvedTypes);
5184+
return $this->transformVoidToNull(TypeCombinator::union(...$resolvedTypes), $methodCall);
51465185
}
51475186

5148-
return $parametersAcceptor->getReturnType();
5187+
return $this->transformVoidToNull($parametersAcceptor->getReturnType(), $methodCall);
51495188
}
51505189

51515190
/** @api */

src/Analyser/Scope.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ public function getType(Expr $node): Type;
7777

7878
public function getNativeType(Expr $expr): Type;
7979

80+
public function getKeepVoidType(Expr $node): Type;
81+
8082
/**
8183
* @deprecated Use getNativeType()
8284
*/

src/Rules/Comparison/UsageOfVoidMatchExpressionRule.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public function getNodeType(): string
2121
public function processNode(Node $node, Scope $scope): array
2222
{
2323
if (!$scope->isInFirstLevelStatement()) {
24-
$matchResultType = $scope->getType($node);
24+
$matchResultType = $scope->getKeepVoidType($node);
2525
if ($matchResultType->isVoid()->yes()) {
2626
return [RuleErrorBuilder::message('Result of match expression (void) is used.')->build()];
2727
}

src/Rules/FunctionCallParametersCheck.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ public function check(
210210
if (
211211
!$funcCall instanceof Node\Expr\New_
212212
&& !$scope->isInFirstLevelStatement()
213-
&& $scope->getType($funcCall)->isVoid()->yes()
213+
&& $scope->getKeepVoidType($funcCall)->isVoid()->yes()
214214
) {
215215
$errors[] = RuleErrorBuilder::message($messages[7])->line($funcCall->getLine())->build();
216216
}

tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6072,15 +6072,15 @@ public function dataVoid(): array
60726072
{
60736073
return [
60746074
[
6075-
'void',
6075+
'null',
60766076
'$this->doFoo()',
60776077
],
60786078
[
6079-
'void',
6079+
'null',
60806080
'$this->doBar()',
60816081
],
60826082
[
6083-
'void',
6083+
'null',
60846084
'$this->doConflictingVoid()',
60856085
],
60866086
];

tests/PHPStan/Analyser/data/conditional-types.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,9 +151,9 @@ abstract public function maybeNever(int $option): void;
151151

152152
public function testMaybeNever(): void
153153
{
154-
assertType('void', $this->maybeNever(0));
154+
assertType('null', $this->maybeNever(0));
155155
assertType('never', $this->maybeNever(1));
156-
assertType('void', $this->maybeNever(2));
156+
assertType('null', $this->maybeNever(2));
157157
}
158158

159159
/**

tests/PHPStan/Analyser/data/emptyiterator.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ public function doFoo(\EmptyIterator $it): void
1111
assertType('EmptyIterator', $it);
1212
assertType('never', $it->key());
1313
assertType('never', $it->current());
14-
assertType('void', $it->next());
14+
assertType('null', $it->next());
1515
assertType('false', $it->valid());
1616
}
1717

tests/PHPStan/Analyser/data/self-out.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,12 @@ function () {
6969
$i = new a(123);
7070
// OK - $i is a<123>
7171
assertType('SelfOut\\a<int>', $i);
72-
assertType('void', $i->test());
72+
assertType('null', $i->test());
7373

7474
$i->addData(321);
7575
// OK - $i is a<123|321>
7676
assertType('SelfOut\\a<int>', $i);
77-
assertType('void', $i->test());
77+
assertType('null', $i->test());
7878

7979
$i->setData("test");
8080
// IfThisIsMismatch - Class is not a<int> as required

tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1584,6 +1584,11 @@ public function testBug10171(): void
15841584
]);
15851585
}
15861586

1587+
public function testBug6720(): void
1588+
{
1589+
$this->analyse([__DIR__ . '/data/bug-6720.php'], []);
1590+
}
1591+
15871592
public function testBug8659(): void
15881593
{
15891594
$this->analyse([__DIR__ . '/data/bug-8659.php'], []);
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug6720;
4+
5+
/** @return string|void */
6+
function a() {}
7+
8+
function b(?string $a): void {}
9+
10+
$a = a();
11+
b($a);

0 commit comments

Comments
 (0)