Skip to content

Commit b070193

Browse files
committed
Remember narrowed types from the constructor when analysing other methods
1 parent 375f68e commit b070193

File tree

4 files changed

+125
-1
lines changed

4 files changed

+125
-1
lines changed

src/Analyser/MutatingScope.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,54 @@ public function enterDeclareStrictTypes(): self
294294
);
295295
}
296296

297+
public function rememberConstructorScope(): self
298+
{
299+
$expressionTypes = [];
300+
foreach ($this->expressionTypes as $exprString => $expressionTypeHolder) {
301+
$expr = $expressionTypeHolder->getExpr();
302+
if (!$expr instanceof ConstFetch) {
303+
continue;
304+
}
305+
$expressionTypes[$exprString] = $expressionTypeHolder;
306+
}
307+
308+
$nativeExpressionTypes = [];
309+
foreach ($this->nativeExpressionTypes as $exprString => $expressionTypeHolder) {
310+
$expr = $expressionTypeHolder->getExpr();
311+
if (!$expr instanceof ConstFetch) {
312+
continue;
313+
}
314+
315+
$nativeExpressionTypes[$exprString] = $expressionTypeHolder;
316+
}
317+
318+
if (array_key_exists('$this', $this->expressionTypes)) {
319+
$expressionTypes['$this'] = $this->expressionTypes['$this'];
320+
}
321+
if (array_key_exists('$this', $this->nativeExpressionTypes)) {
322+
$nativeExpressionTypes['$this'] = $this->nativeExpressionTypes['$this'];
323+
}
324+
325+
return $this->scopeFactory->create(
326+
$this->context,
327+
$this->isDeclareStrictTypes(),
328+
$this->getFunction(),
329+
$this->getNamespace(),
330+
$expressionTypes,
331+
$nativeExpressionTypes,
332+
$this->conditionalExpressions,
333+
$this->inClosureBindScopeClasses,
334+
$this->anonymousFunctionReflection,
335+
$this->inFirstLevelStatement,
336+
[],
337+
[],
338+
$this->inFunctionCallsStack,
339+
$this->afterExtractCall,
340+
$this->parentScope,
341+
$this->nativeTypesPromoted,
342+
);
343+
}
344+
297345
/** @api */
298346
public function isInClass(): bool
299347
{

src/Analyser/NodeScopeResolver.php

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@
218218
use function str_starts_with;
219219
use function strtolower;
220220
use function trim;
221+
use function usort;
221222
use const PHP_VERSION_ID;
222223
use const SORT_NUMERIC;
223224

@@ -791,6 +792,10 @@ private function processStmtNode(
791792
$classReflection,
792793
$methodReflection,
793794
), $methodScope);
795+
796+
if ($isConstructor) {
797+
$scope = $statementResult->getScope()->rememberConstructorScope();
798+
}
794799
}
795800
} elseif ($stmt instanceof Echo_) {
796801
$hasYield = false;
@@ -925,7 +930,17 @@ private function processStmtNode(
925930
$classStatementsGatherer = new ClassStatementsGatherer($classReflection, $nodeCallback);
926931
$this->processAttributeGroups($stmt, $stmt->attrGroups, $classScope, $classStatementsGatherer);
927932

928-
$this->processStmtNodes($stmt, $stmt->stmts, $classScope, $classStatementsGatherer, $context);
933+
// analyze static methods first, constructor next and instance methods last so we can carry over the scope
934+
$classLikeStatements = $stmt->stmts;
935+
usort($classLikeStatements, static function ($a, $b) {
936+
if (!$a instanceof Node\Stmt\ClassMethod || !$b instanceof Node\Stmt\ClassMethod) {
937+
return 0;
938+
}
939+
940+
return [!$a->isStatic(), $a->name->toLowerString() !== '__construct'] <=> [!$b->isStatic(), $b->name->toLowerString() !== '__construct'];
941+
});
942+
943+
$this->processStmtNodes($stmt, $classLikeStatements, $classScope, $classStatementsGatherer, $context);
929944
$nodeCallback(new ClassPropertiesNode($stmt, $this->readWritePropertiesExtensionProvider, $classStatementsGatherer->getProperties(), $classStatementsGatherer->getPropertyUsages(), $classStatementsGatherer->getMethodCalls(), $classStatementsGatherer->getReturnStatementsNodes(), $classStatementsGatherer->getPropertyAssigns(), $classReflection), $classScope);
930945
$nodeCallback(new ClassMethodsNode($stmt, $classStatementsGatherer->getMethods(), $classStatementsGatherer->getMethodCalls(), $classReflection), $classScope);
931946
$nodeCallback(new ClassConstantsNode($stmt, $classStatementsGatherer->getConstants(), $classStatementsGatherer->getConstantFetches(), $classReflection), $classScope);

tests/PHPStan/Rules/Constants/ConstantRuleTest.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,20 @@ public function testDefinedScopeMerge(): void
8080
]);
8181
}
8282

83+
public function testRememberedConstructorScope(): void
84+
{
85+
$this->analyse([__DIR__ . '/data/remembered-constructor-scope.php'], [
86+
[
87+
'Constant REMEMBERED_FOO not found.',
88+
23,
89+
'Learn more at https://phpstan.org/user-guide/discovering-symbols',
90+
],
91+
[
92+
'Constant REMEMBERED_FOO not found.',
93+
38,
94+
'Learn more at https://phpstan.org/user-guide/discovering-symbols',
95+
],
96+
]);
97+
}
98+
8399
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace RememberedConstructorScope;
4+
5+
use LogicException;
6+
7+
class HelloWorld
8+
{
9+
public function dooFoo(): void
10+
{
11+
if (REMEMBERED_FOO === '3') {
12+
13+
}
14+
}
15+
16+
public function returnFoo(): string
17+
{
18+
return REMEMBERED_FOO;
19+
}
20+
21+
static public function staticFoo(): void
22+
{
23+
echo REMEMBERED_FOO; // should error, as can be invoked without instantiation
24+
}
25+
26+
public function __construct()
27+
{
28+
if (!defined('REMEMBERED_FOO')) {
29+
throw new LogicException();
30+
}
31+
if (!is_string(REMEMBERED_FOO)) {
32+
throw new LogicException();
33+
}
34+
}
35+
36+
static public function staticFoo2(): void
37+
{
38+
echo REMEMBERED_FOO; // should error, as can be invoked without instantiation
39+
}
40+
41+
public function returnFoo2(): string
42+
{
43+
return REMEMBERED_FOO;
44+
}
45+
}

0 commit comments

Comments
 (0)