Skip to content

Commit 4cbde15

Browse files
committed
Merge remote-tracking branch 'origin/2.1.x' into 2.2.x
2 parents 7b73e0a + 43c1a1b commit 4cbde15

29 files changed

+547
-114
lines changed

changelog-generator/run.php

Lines changed: 7 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
require_once __DIR__ . '/vendor/autoload.php';
77

8-
use Github\Api\GraphQL;
98
use Github\Api\PullRequest;
109
use Github\Api\Repo;
1110
use Github\Api\Search;
@@ -29,6 +28,7 @@
2928
use function exec;
3029
use function explode;
3130
use function implode;
31+
use function preg_match_all;
3232
use function preg_replace;
3333
use function sprintf;
3434

@@ -67,9 +67,6 @@ protected function execute(InputInterface $input, OutputInterface $output)
6767
/** @var PullRequest $pullRequestApi */
6868
$pullRequestApi = $gitHubClient->api('pull_request');
6969

70-
/** @var GraphQL $graphqlApi */
71-
$graphqlApi = $gitHubClient->api('graphql');
72-
7370
$command = ['git', 'log', sprintf('%s..%s', $input->getArgument('fromCommit'), $input->getArgument('toCommit'))];
7471
$excludeBranch = $input->getOption('exclude-branch');
7572
if ($excludeBranch !== null) {
@@ -135,31 +132,13 @@ protected function execute(InputInterface $input, OutputInterface $output)
135132
'user' => $pullRequests[0]['user'],
136133
],
137134
];
138-
$autoclosedIssues = $graphqlApi->execute(
139-
<<<'QUERY'
140-
query ($owner:String!, $repo:String!, $pr:Int!){
141-
repository(owner:$owner, name:$repo){
142-
pullRequest(number:$pr){
143-
closingIssuesReferences(first:100){
144-
nodes { number title url repository { nameWithOwner } }
145-
}
146-
}
147-
}
148-
},
149-
QUERY,
150-
[
151-
'owner' => 'phpstan',
152-
'repo' => 'phpstan-src',
153-
'pr' => $pullRequests[0]['number'],
154-
],
155-
);
156-
foreach ($autoclosedIssues['data']['repository']['pullRequest']['closingIssuesReferences']['nodes'] as $closedIssue) {
157-
if ($closedIssue['repository']['nameWithOwner'] !== 'phpstan/phpstan') {
158-
continue;
135+
$prBody = $pullRequests[0]['body'] ?? '';
136+
if (preg_match_all('/(?:closes?|fix(?:es)?)\s+(?:https:\/\/github\.com\/phpstan\/phpstan\/issues\/|phpstan\/phpstan#)(\d+)/i', $prBody, $matches)) {
137+
foreach (array_unique($matches[1]) as $issueNumber) {
138+
$items[] = [
139+
'number' => (int) $issueNumber,
140+
];
159141
}
160-
$items[] = [
161-
'number' => $closedIssue['number'],
162-
];
163142
}
164143
} else {
165144
$items = $searchApi->issues(sprintf('repo:phpstan/phpstan %s is:issue', $commit['hash']), 'created')['items'];

src/Analyser/ExprHandler/MethodCallHandler.php

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -153,24 +153,22 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
153153
$scope = $argsResult->getScope();
154154

155155
if ($methodReflection !== null) {
156-
if ($parametersAcceptor !== null) {
157-
$methodThrowPoint = $this->getMethodThrowPoint($methodReflection, $parametersAcceptor, $expr, $scope);
158-
if ($methodThrowPoint !== null) {
159-
$throwPoints[] = $methodThrowPoint;
160-
}
156+
$methodThrowPoint = $this->getMethodThrowPoint($methodReflection, $parametersAcceptor, $expr, $scope);
157+
if ($methodThrowPoint !== null) {
158+
$throwPoints[] = $methodThrowPoint;
161159
}
162160

163161
if ($methodReflection->getName() === '__construct' || $methodReflection->hasSideEffects()->yes()) {
164162
$nodeScopeResolver->callNodeCallback($nodeCallback, new InvalidateExprNode($normalizedExpr->var), $scope, $storage);
165163
$scope = $scope->invalidateExpression($normalizedExpr->var, true, $methodReflection->getDeclaringClass());
166-
} elseif ($this->rememberPossiblyImpureFunctionValues && $methodReflection->hasSideEffects()->maybe() && !$methodReflection->getDeclaringClass()->isBuiltin() && $parametersAcceptor !== null) {
164+
} elseif ($this->rememberPossiblyImpureFunctionValues && $methodReflection->hasSideEffects()->maybe() && !$methodReflection->getDeclaringClass()->isBuiltin()) {
167165
$scope = $scope->assignExpression(
168166
new PossiblyImpureCallExpr($normalizedExpr, $normalizedExpr->var, sprintf('%s::%s()', $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName())),
169167
$parametersAcceptor->getReturnType(),
170168
new MixedType(),
171169
);
172170
}
173-
if ($parametersAcceptor !== null && !$methodReflection->isStatic()) {
171+
if (!$methodReflection->isStatic()) {
174172
$selfOutType = $methodReflection->getSelfOutType();
175173
if ($selfOutType !== null) {
176174
$scope = $scope->assignExpression(

src/Analyser/ExprHandler/StaticCallHandler.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
198198
$scope = $argsResult->getScope();
199199
$scopeFunction = $scope->getFunction();
200200

201-
if ($methodReflection !== null && $parametersAcceptor !== null) {
201+
if ($methodReflection !== null) {
202202
$methodThrowPoint = $this->getStaticMethodThrowPoint($methodReflection, $parametersAcceptor, $expr, $scope);
203203
if ($methodThrowPoint !== null) {
204204
$throwPoints[] = $methodThrowPoint;
@@ -221,7 +221,6 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
221221
} elseif (
222222
$methodReflection !== null
223223
&& $this->rememberPossiblyImpureFunctionValues
224-
&& $parametersAcceptor !== null
225224
&& $scope->isInClass()
226225
&& $scope->getClassReflection()->is($methodReflection->getDeclaringClass()->getName())
227226
&& $methodReflection->hasSideEffects()->maybe()

src/Analyser/MutatingScope.php

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3235,7 +3235,7 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self
32353235
} else {
32363236
$scope = $scope->removeTypeFromExpression($expr, $type);
32373237
}
3238-
$specifiedExpressions[$typeSpecification['exprString']] = ExpressionTypeHolder::createYes($expr, $scope->getType($expr));
3238+
$specifiedExpressions[$typeSpecification['exprString']] = ExpressionTypeHolder::createYes($expr, $scope->getScopeType($expr));
32393239
}
32403240

32413241
$conditions = [];
@@ -3368,7 +3368,7 @@ public function isInFirstLevelStatement(): bool
33683368
return $this->inFirstLevelStatement;
33693369
}
33703370

3371-
public function mergeWith(?self $otherScope): self
3371+
public function mergeWith(?self $otherScope, bool $preserveVacuousConditionals = false): self
33723372
{
33733373
if ($otherScope === null || $this === $otherScope) {
33743374
return $this;
@@ -3378,6 +3378,18 @@ public function mergeWith(?self $otherScope): self
33783378

33793379
$mergedExpressionTypes = $this->mergeVariableHolders($ourExpressionTypes, $theirExpressionTypes);
33803380
$conditionalExpressions = $this->intersectConditionalExpressions($otherScope->conditionalExpressions);
3381+
if ($preserveVacuousConditionals) {
3382+
$conditionalExpressions = $this->preserveVacuousConditionalExpressions(
3383+
$conditionalExpressions,
3384+
$this->conditionalExpressions,
3385+
$theirExpressionTypes,
3386+
);
3387+
$conditionalExpressions = $this->preserveVacuousConditionalExpressions(
3388+
$conditionalExpressions,
3389+
$otherScope->conditionalExpressions,
3390+
$ourExpressionTypes,
3391+
);
3392+
}
33813393
$conditionalExpressions = $this->createConditionalExpressions(
33823394
$conditionalExpressions,
33833395
$ourExpressionTypes,
@@ -3483,6 +3495,48 @@ private function intersectConditionalExpressions(array $otherConditionalExpressi
34833495
return $newConditionalExpressions;
34843496
}
34853497

3498+
/**
3499+
* @param array<string, ConditionalExpressionHolder[]> $currentConditionalExpressions
3500+
* @param array<string, ConditionalExpressionHolder[]> $sourceConditionalExpressions
3501+
* @param array<string, ExpressionTypeHolder> $otherExpressionTypes
3502+
* @return array<string, ConditionalExpressionHolder[]>
3503+
*/
3504+
private function preserveVacuousConditionalExpressions(
3505+
array $currentConditionalExpressions,
3506+
array $sourceConditionalExpressions,
3507+
array $otherExpressionTypes,
3508+
): array
3509+
{
3510+
foreach ($sourceConditionalExpressions as $exprString => $holders) {
3511+
foreach ($holders as $key => $holder) {
3512+
if (isset($currentConditionalExpressions[$exprString][$key])) {
3513+
continue;
3514+
}
3515+
3516+
$typeHolder = $holder->getTypeHolder();
3517+
if ($typeHolder->getCertainty()->no() && !$typeHolder->getExpr() instanceof Variable) {
3518+
continue;
3519+
}
3520+
3521+
foreach ($holder->getConditionExpressionTypeHolders() as $guardExprString => $guardTypeHolder) {
3522+
if (!array_key_exists($guardExprString, $otherExpressionTypes)) {
3523+
continue;
3524+
}
3525+
3526+
$otherType = $otherExpressionTypes[$guardExprString]->getType();
3527+
$guardType = $guardTypeHolder->getType();
3528+
3529+
if ($otherType->isSuperTypeOf($guardType)->no()) {
3530+
$currentConditionalExpressions[$exprString][$key] = $holder;
3531+
break;
3532+
}
3533+
}
3534+
}
3535+
}
3536+
3537+
return $currentConditionalExpressions;
3538+
}
3539+
34863540
/**
34873541
* @param array<string, ConditionalExpressionHolder[]> $newConditionalExpressions
34883542
* @param array<string, ConditionalExpressionHolder[]> $existingConditionalExpressions

src/Analyser/NodeScopeResolver.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1146,7 +1146,7 @@ public function processStmtNode(
11461146
$throwPoints = array_merge($throwPoints, $branchScopeStatementResult->getThrowPoints());
11471147
$impurePoints = array_merge($impurePoints, $branchScopeStatementResult->getImpurePoints());
11481148
$branchScope = $branchScopeStatementResult->getScope();
1149-
$finalScope = $branchScopeStatementResult->isAlwaysTerminating() ? $finalScope : $branchScope->mergeWith($finalScope);
1149+
$finalScope = $branchScopeStatementResult->isAlwaysTerminating() ? $finalScope : $branchScope->mergeWith($finalScope, true);
11501150
$alwaysTerminating = $alwaysTerminating && $branchScopeStatementResult->isAlwaysTerminating();
11511151
if (count($branchScopeStatementResult->getEndStatements()) > 0) {
11521152
$endStatements = array_merge($endStatements, $branchScopeStatementResult->getEndStatements());
@@ -1170,7 +1170,7 @@ public function processStmtNode(
11701170

11711171
if ($stmt->else === null) {
11721172
if (!$ifAlwaysTrue && !$lastElseIfConditionIsTrue) {
1173-
$finalScope = $scope->mergeWith($finalScope);
1173+
$finalScope = $scope->mergeWith($finalScope, true);
11741174
$alwaysTerminating = false;
11751175
}
11761176
} else {
@@ -1182,7 +1182,7 @@ public function processStmtNode(
11821182
$throwPoints = array_merge($throwPoints, $branchScopeStatementResult->getThrowPoints());
11831183
$impurePoints = array_merge($impurePoints, $branchScopeStatementResult->getImpurePoints());
11841184
$branchScope = $branchScopeStatementResult->getScope();
1185-
$finalScope = $branchScopeStatementResult->isAlwaysTerminating() ? $finalScope : $branchScope->mergeWith($finalScope);
1185+
$finalScope = $branchScopeStatementResult->isAlwaysTerminating() ? $finalScope : $branchScope->mergeWith($finalScope, true);
11861186
$alwaysTerminating = $alwaysTerminating && $branchScopeStatementResult->isAlwaysTerminating();
11871187
if (count($branchScopeStatementResult->getEndStatements()) > 0) {
11881188
$endStatements = array_merge($endStatements, $branchScopeStatementResult->getEndStatements());

src/Analyser/TypeSpecifier.php

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1896,7 +1896,7 @@ private function specifyTypesFromCallableCall(TypeSpecifierContext $context, Fun
18961896
}
18971897
}
18981898

1899-
if ($assertions === null || $assertions->getAll() === [] || $parametersAcceptor === null) {
1899+
if ($assertions === null || $assertions->getAll() === []) {
19001900
return null;
19011901
}
19021902

@@ -2231,6 +2231,30 @@ private function createForExpr(
22312231
}
22322232
}
22332233

2234+
if (
2235+
$expr instanceof FuncCall
2236+
&& !$expr->name instanceof Name
2237+
) {
2238+
$nameType = $scope->getType($expr->name);
2239+
if ($nameType->isCallable()->yes()) {
2240+
$isPure = null;
2241+
foreach ($nameType->getCallableParametersAcceptors($scope) as $variant) {
2242+
$variantIsPure = $variant->isPure();
2243+
$isPure = $isPure === null ? $variantIsPure : $isPure->and($variantIsPure);
2244+
}
2245+
2246+
if ($isPure !== null) {
2247+
if ($isPure->no()) {
2248+
return new SpecifiedTypes([], []);
2249+
}
2250+
2251+
if (!$this->rememberPossiblyImpureFunctionValues && !$isPure->yes()) {
2252+
return new SpecifiedTypes([], []);
2253+
}
2254+
}
2255+
}
2256+
}
2257+
22342258
if (
22352259
$expr instanceof MethodCall
22362260
&& $expr->name instanceof Node\Identifier

src/Php/PhpVersion.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,4 +501,9 @@ public function supportsObjectsInArraySumProduct(): bool
501501
return $this->versionId >= 80300;
502502
}
503503

504+
public function hasFilterThrowOnFailureConstant(): bool
505+
{
506+
return $this->versionId >= 80500;
507+
}
508+
504509
}

src/Reflection/ClassReflection.php

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
use PHPStan\Type\Generic\TemplateTypeVariance;
4949
use PHPStan\Type\Generic\TemplateTypeVarianceMap;
5050
use PHPStan\Type\Generic\TypeProjectionHelper;
51+
use PHPStan\Type\MixedType;
5152
use PHPStan\Type\ObjectType;
5253
use PHPStan\Type\Type;
5354
use PHPStan\Type\TypeAlias;
@@ -237,7 +238,7 @@ public function getParentClass(): ?ClassReflection
237238
if ($this->isGeneric()) {
238239
$extendedType = TemplateTypeHelper::resolveTemplateTypes(
239240
$extendedType,
240-
$this->getPossiblyIncompleteActiveTemplateTypeMap(),
241+
$this->getActiveTemplateTypeMapForAncestorResolution(),
241242
$this->getCallSiteVarianceMap(),
242243
TemplateTypeVariance::createStatic(),
243244
);
@@ -1164,7 +1165,7 @@ public function getImmediateInterfaces(): array
11641165
if ($this->isGeneric()) {
11651166
$implementedType = TemplateTypeHelper::resolveTemplateTypes(
11661167
$implementedType,
1167-
$this->getPossiblyIncompleteActiveTemplateTypeMap(),
1168+
$this->getActiveTemplateTypeMapForAncestorResolution(),
11681169
$this->getCallSiteVarianceMap(),
11691170
TemplateTypeVariance::createStatic(),
11701171
true,
@@ -1686,6 +1687,37 @@ public function getPossiblyIncompleteActiveTemplateTypeMap(): TemplateTypeMap
16861687
return $this->resolvedTemplateTypeMap ?? $this->getTemplateTypeMap();
16871688
}
16881689

1690+
/**
1691+
* Returns a template type map for resolving ancestor type declarations (@extends, @implements).
1692+
* Like getPossiblyIncompleteActiveTemplateTypeMap(), but resolves ErrorType entries
1693+
* to their template bounds when the bound is not mixed. This ensures that when a child
1694+
* class narrows a template bound (e.g. `@template T of SpecificType`), the narrowed bound
1695+
* is propagated to ancestor declarations instead of being lost as ErrorType.
1696+
*/
1697+
private function getActiveTemplateTypeMapForAncestorResolution(): TemplateTypeMap
1698+
{
1699+
$map = $this->getPossiblyIncompleteActiveTemplateTypeMap();
1700+
$templateTypeMap = $this->getTemplateTypeMap();
1701+
1702+
return $map->map(static function (string $name, Type $type) use ($templateTypeMap): Type {
1703+
if (!$type instanceof ErrorType) {
1704+
return $type;
1705+
}
1706+
1707+
$templateType = $templateTypeMap->getType($name);
1708+
if (!$templateType instanceof TemplateType) {
1709+
return $type;
1710+
}
1711+
1712+
$bound = $templateType->getBound();
1713+
if ($bound instanceof MixedType) {
1714+
return $type;
1715+
}
1716+
1717+
return TemplateTypeHelper::resolveToDefaults($templateType);
1718+
});
1719+
}
1720+
16891721
private function getDefaultCallSiteVarianceMap(): TemplateTypeVarianceMap
16901722
{
16911723
if ($this->defaultCallSiteVarianceMap !== null) {

src/Rules/Functions/FilterVarRule.php

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use PhpParser\Node\Name;
88
use PHPStan\Analyser\Scope;
99
use PHPStan\DependencyInjection\RegisteredRule;
10+
use PHPStan\Php\PhpVersion;
1011
use PHPStan\Reflection\ReflectionProvider;
1112
use PHPStan\Rules\Rule;
1213
use PHPStan\Rules\RuleErrorBuilder;
@@ -23,6 +24,7 @@ final class FilterVarRule implements Rule
2324
public function __construct(
2425
private ReflectionProvider $reflectionProvider,
2526
private FilterFunctionReturnTypeHelper $filterFunctionReturnTypeHelper,
27+
private PhpVersion $phpVersion,
2628
)
2729
{
2830
}
@@ -44,23 +46,28 @@ public function processNode(Node $node, Scope $scope): array
4446

4547
$args = $node->getArgs();
4648

47-
if ($this->reflectionProvider->hasConstant(new Name\FullyQualified('FILTER_THROW_ON_FAILURE'), null)) {
48-
if (count($args) < 3) {
49-
return [];
50-
}
49+
if (count($args) < 3) {
50+
return [];
51+
}
52+
53+
if (
54+
!$this->phpVersion->hasFilterThrowOnFailureConstant()
55+
|| !$this->reflectionProvider->hasConstant(new Name\FullyQualified('FILTER_THROW_ON_FAILURE'), null)
56+
) {
57+
return [];
58+
}
5159

52-
$flagsType = $scope->getType($args[2]->value);
60+
$flagsType = $scope->getType($args[2]->value);
5361

54-
if ($this->filterFunctionReturnTypeHelper->hasFlag('FILTER_NULL_ON_FAILURE', $flagsType)
55-
->and($this->filterFunctionReturnTypeHelper->hasFlag('FILTER_THROW_ON_FAILURE', $flagsType))
56-
->yes()
57-
) {
58-
return [
59-
RuleErrorBuilder::message('Cannot use both FILTER_NULL_ON_FAILURE and FILTER_THROW_ON_FAILURE.')
60-
->identifier('filterVar.nullOnFailureAndThrowOnFailure')
61-
->build(),
62-
];
63-
}
62+
if ($this->filterFunctionReturnTypeHelper->hasFlag('FILTER_NULL_ON_FAILURE', $flagsType)
63+
->and($this->filterFunctionReturnTypeHelper->hasFlag('FILTER_THROW_ON_FAILURE', $flagsType))
64+
->yes()
65+
) {
66+
return [
67+
RuleErrorBuilder::message('Cannot use both FILTER_NULL_ON_FAILURE and FILTER_THROW_ON_FAILURE.')
68+
->identifier('filterVar.nullOnFailureAndThrowOnFailure')
69+
->build(),
70+
];
6471
}
6572

6673
return [];

0 commit comments

Comments
 (0)