Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 04f8636

Browse files
Bleeding edge - report "missing return" error closer to where the return is missing
This also fixes false positives about `@param-out`
1 parent 28dff17 commit 04f8636

14 files changed

+203
-15
lines changed

‎conf/bleedingEdge.neon‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,5 +57,6 @@ parameters:
5757
narrowPregMatches: true
5858
uselessReturnValue: true
5959
printfArrayParameters: true
60+
preciseMissingReturn: true
6061
stubFiles:
6162
- ../stubs/bleedingEdge/Rule.stub

‎conf/config.neon‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ parameters:
9292
narrowPregMatches: false
9393
uselessReturnValue: false
9494
printfArrayParameters: false
95+
preciseMissingReturn: false
9596
fileExtensions:
9697
- php
9798
checkAdvancedIsset: false
@@ -535,6 +536,7 @@ services:
535536
detectDeadTypeInMultiCatch: %featureToggles.detectDeadTypeInMultiCatch%
536537
universalObjectCratesClasses: %universalObjectCratesClasses%
537538
paramOutType: %featureToggles.paramOutType%
539+
preciseMissingReturn: %featureToggles.preciseMissingReturn%
538540

539541
-
540542
class: PHPStan\Analyser\ConstantResolver

‎conf/parametersSchema.neon‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ parametersSchema:
8787
narrowPregMatches: bool()
8888
uselessReturnValue: bool()
8989
printfArrayParameters: bool()
90+
preciseMissingReturn: bool()
9091
])
9192
fileExtensions: listOf(string())
9293
checkAdvancedIsset: bool()

‎src/Analyser/EndStatementResult.php‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Analyser;
4+
5+
use PhpParser\Node\Stmt;
6+
7+
class EndStatementResult
8+
{
9+
10+
public function __construct(
11+
private Stmt $statement,
12+
private StatementResult $result,
13+
)
14+
{
15+
}
16+
17+
public function getStatement(): Stmt
18+
{
19+
return $this->statement;
20+
}
21+
22+
public function getResult(): StatementResult
23+
{
24+
return $this->result;
25+
}
26+
27+
}

‎src/Analyser/NodeScopeResolver.php‎

Lines changed: 60 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ public function __construct(
261261
private readonly bool $treatPhpDocTypesAsCertain,
262262
private readonly bool $detectDeadTypeInMultiCatch,
263263
private readonly bool $paramOutType,
264+
private readonly bool $preciseMissingReturn,
264265
)
265266
{
266267
$earlyTerminatingMethodNames = [];
@@ -360,18 +361,38 @@ public function processStmtNodes(
360361
if ($shouldCheckLastStatement && $isLast) {
361362
/** @var Node\Stmt\Function_|Node\Stmt\ClassMethod|Expr\Closure $parentNode */
362363
$parentNode = $parentNode;
363-
$nodeCallback(new ExecutionEndNode(
364-
$stmt,
365-
new StatementResult(
366-
$scope,
367-
$hasYield,
368-
$statementResult->isAlwaysTerminating(),
369-
$statementResult->getExitPoints(),
370-
$statementResult->getThrowPoints(),
371-
$statementResult->getImpurePoints(),
372-
),
373-
$parentNode->returnType !== null,
374-
), $scope);
364+
365+
$endStatements = $statementResult->getEndStatements();
366+
if ($this->preciseMissingReturn && count($endStatements) > 0) {
367+
foreach ($endStatements as $endStatement) {
368+
$endStatementResult = $endStatement->getResult();
369+
$nodeCallback(new ExecutionEndNode(
370+
$endStatement->getStatement(),
371+
new StatementResult(
372+
$endStatementResult->getScope(),
373+
$hasYield,
374+
$endStatementResult->isAlwaysTerminating(),
375+
$endStatementResult->getExitPoints(),
376+
$endStatementResult->getThrowPoints(),
377+
$endStatementResult->getImpurePoints(),
378+
),
379+
$parentNode->returnType !== null,
380+
), $endStatementResult->getScope());
381+
}
382+
} else {
383+
$nodeCallback(new ExecutionEndNode(
384+
$stmt,
385+
new StatementResult(
386+
$scope,
387+
$hasYield,
388+
$statementResult->isAlwaysTerminating(),
389+
$statementResult->getExitPoints(),
390+
$statementResult->getThrowPoints(),
391+
$statementResult->getImpurePoints(),
392+
),
393+
$parentNode->returnType !== null,
394+
), $scope);
395+
}
375396
}
376397

377398
$exitPoints = array_merge($exitPoints, $statementResult->getExitPoints());
@@ -915,6 +936,7 @@ private function processStmtNode(
915936
$exitPoints = [];
916937
$throwPoints = $overridingThrowPoints ?? $condResult->getThrowPoints();
917938
$impurePoints = $condResult->getImpurePoints();
939+
$endStatements = [];
918940
$finalScope = null;
919941
$alwaysTerminating = true;
920942
$hasYield = $condResult->hasYield();
@@ -928,6 +950,13 @@ private function processStmtNode(
928950
$branchScope = $branchScopeStatementResult->getScope();
929951
$finalScope = $branchScopeStatementResult->isAlwaysTerminating() ? null : $branchScope;
930952
$alwaysTerminating = $branchScopeStatementResult->isAlwaysTerminating();
953+
if (count($branchScopeStatementResult->getEndStatements()) > 0) {
954+
$endStatements = array_merge($endStatements, $branchScopeStatementResult->getEndStatements());
955+
} elseif (count($stmt->stmts) > 0) {
956+
$endStatements[] = new EndStatementResult($stmt->stmts[count($stmt->stmts) - 1], $branchScopeStatementResult);
957+
} else {
958+
$endStatements[] = new EndStatementResult($stmt, $branchScopeStatementResult);
959+
}
931960
$hasYield = $branchScopeStatementResult->hasYield() || $hasYield;
932961
}
933962

@@ -960,6 +989,13 @@ private function processStmtNode(
960989
$branchScope = $branchScopeStatementResult->getScope();
961990
$finalScope = $branchScopeStatementResult->isAlwaysTerminating() ? $finalScope : $branchScope->mergeWith($finalScope);
962991
$alwaysTerminating = $alwaysTerminating && $branchScopeStatementResult->isAlwaysTerminating();
992+
if (count($branchScopeStatementResult->getEndStatements()) > 0) {
993+
$endStatements = array_merge($endStatements, $branchScopeStatementResult->getEndStatements());
994+
} elseif (count($elseif->stmts) > 0) {
995+
$endStatements[] = new EndStatementResult($elseif->stmts[count($elseif->stmts) - 1], $branchScopeStatementResult);
996+
} else {
997+
$endStatements[] = new EndStatementResult($elseif, $branchScopeStatementResult);
998+
}
963999
$hasYield = $hasYield || $branchScopeStatementResult->hasYield();
9641000
}
9651001

@@ -989,6 +1025,13 @@ private function processStmtNode(
9891025
$branchScope = $branchScopeStatementResult->getScope();
9901026
$finalScope = $branchScopeStatementResult->isAlwaysTerminating() ? $finalScope : $branchScope->mergeWith($finalScope);
9911027
$alwaysTerminating = $alwaysTerminating && $branchScopeStatementResult->isAlwaysTerminating();
1028+
if (count($branchScopeStatementResult->getEndStatements()) > 0) {
1029+
$endStatements = array_merge($endStatements, $branchScopeStatementResult->getEndStatements());
1030+
} elseif (count($stmt->else->stmts) > 0) {
1031+
$endStatements[] = new EndStatementResult($stmt->else->stmts[count($stmt->else->stmts) - 1], $branchScopeStatementResult);
1032+
} else {
1033+
$endStatements[] = new EndStatementResult($stmt->else, $branchScopeStatementResult);
1034+
}
9921035
$hasYield = $hasYield || $branchScopeStatementResult->hasYield();
9931036
}
9941037
}
@@ -997,7 +1040,11 @@ private function processStmtNode(
9971040
$finalScope = $scope;
9981041
}
9991042

1000-
return new StatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPoints, $throwPoints, $impurePoints);
1043+
if ($stmt->else === null && !$ifAlwaysTrue && !$lastElseIfConditionIsTrue) {
1044+
$endStatements[] = new EndStatementResult($stmt, new StatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPoints, $throwPoints, $impurePoints));
1045+
}
1046+
1047+
return new StatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPoints, $throwPoints, $impurePoints, $endStatements);
10011048
} elseif ($stmt instanceof Node\Stmt\TraitUse) {
10021049
$hasYield = false;
10031050
$throwPoints = [];

‎src/Analyser/StatementResult.php‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ class StatementResult
1313
* @param StatementExitPoint[] $exitPoints
1414
* @param ThrowPoint[] $throwPoints
1515
* @param ImpurePoint[] $impurePoints
16+
* @param EndStatementResult[] $endStatements
1617
*/
1718
public function __construct(
1819
private MutatingScope $scope,
@@ -21,6 +22,7 @@ public function __construct(
2122
private array $exitPoints,
2223
private array $throwPoints,
2324
private array $impurePoints,
25+
private array $endStatements = [],
2426
)
2527
{
2628
}
@@ -165,4 +167,25 @@ public function getImpurePoints(): array
165167
return $this->impurePoints;
166168
}
167169

170+
/**
171+
* Top-level StatementResult represents the state of the code
172+
* at the end of control flow statements like If_ or TryCatch.
173+
*
174+
* It shows how Scope etc. looks like after If_ no matter
175+
* which code branch was executed.
176+
*
177+
* For If_, "end statements" contain the state of the code
178+
* at the end of each branch - if, elseifs, else, including the last
179+
* statement node in each branch.
180+
*
181+
* For nested ifs, end statements try to contain the last non-control flow
182+
* statement like Return_ or Throw_, instead of If_, TryCatch, or Foreach_.
183+
*
184+
* @return EndStatementResult[]
185+
*/
186+
public function getEndStatements(): array
187+
{
188+
return $this->endStatements;
189+
}
190+
168191
}

‎src/Rules/Variables/ParameterOutExecutionEndTypeRule.php‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use PHPStan\Rules\RuleErrorBuilder;
1616
use PHPStan\Rules\RuleLevelHelper;
1717
use PHPStan\Type\ErrorType;
18+
use PHPStan\Type\NeverType;
1819
use PHPStan\Type\Type;
1920
use PHPStan\Type\TypeUtils;
2021
use PHPStan\Type\VerbosityLevel;
@@ -48,6 +49,18 @@ public function processNode(Node $node, Scope $scope): array
4849
return [];
4950
}
5051

52+
$endNode = $node->getNode();
53+
if ($endNode instanceof Node\Stmt\Expression) {
54+
$endNodeExpr = $endNode->expr;
55+
$endNodeExprType = $scope->getType($endNodeExpr);
56+
if ($endNodeExprType instanceof NeverType && $endNodeExprType->isExplicit()) {
57+
return [];
58+
}
59+
}
60+
if ($endNode instanceof Node\Stmt\Throw_) {
61+
return [];
62+
}
63+
5164
$variant = ParametersAcceptorSelector::selectSingle($inFunction->getVariants());
5265
$parameters = $variant->getParameters();
5366
$errors = [];

‎src/Testing/RuleTestCase.php‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ private function getAnalyser(DirectRuleRegistry $ruleRegistry): Analyser
106106
$this->shouldTreatPhpDocTypesAsCertain(),
107107
self::getContainer()->getParameter('featureToggles')['detectDeadTypeInMultiCatch'],
108108
self::getContainer()->getParameter('featureToggles')['paramOutType'],
109+
self::getContainer()->getParameter('featureToggles')['preciseMissingReturn'],
109110
);
110111
$fileAnalyser = new FileAnalyser(
111112
$this->createScopeFactory($reflectionProvider, $typeSpecifier),

‎src/Testing/TypeInferenceTestCase.php‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ public static function processFile(
8686
self::getContainer()->getParameter('treatPhpDocTypesAsCertain'),
8787
self::getContainer()->getParameter('featureToggles')['detectDeadTypeInMultiCatch'],
8888
self::getContainer()->getParameter('featureToggles')['paramOutType'],
89+
self::getContainer()->getParameter('featureToggles')['preciseMissingReturn'],
8990
);
9091
$resolver->setAnalysedFiles(array_map(static fn (string $file): string => $fileHelper->normalizePath($file), array_merge([$file], static::getAdditionalAnalysedFiles())));
9192

‎tests/PHPStan/Analyser/AnalyserTest.php‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -742,6 +742,7 @@ private function createAnalyser(bool $enableIgnoreErrorsWithinPhpDocs): Analyser
742742
$this->shouldTreatPhpDocTypesAsCertain(),
743743
self::getContainer()->getParameter('featureToggles')['detectDeadTypeInMultiCatch'],
744744
self::getContainer()->getParameter('featureToggles')['paramOutType'],
745+
self::getContainer()->getParameter('featureToggles')['preciseMissingReturn'],
745746
);
746747
$lexer = new Lexer(['usedAttributes' => ['comments', 'startLine', 'endLine', 'startTokenPos', 'endTokenPos']]);
747748
$fileAnalyser = new FileAnalyser(

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /