Skip to content

Commit 0264f5b

Browse files
committed
Bleeding edge - IncompatibleDefaultParameterTypeRule for closures
1 parent c4ee0b8 commit 0264f5b

11 files changed

+259
-2
lines changed

conf/bleedingEdge.neon

+1
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,4 @@ parameters:
2828
alwaysTrueAlwaysReported: true
2929
disableUnreachableBranchesRules: true
3030
varTagType: true
31+
closureDefaultParameterTypeRule: true

conf/config.level2.neon

+8
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ rules:
4545
- PHPStan\Rules\Properties\AccessPrivatePropertyThroughStaticRule
4646

4747
conditionalTags:
48+
PHPStan\Rules\Functions\IncompatibleArrowFunctionDefaultParameterTypeRule:
49+
phpstan.rules.rule: %featureToggles.closureDefaultParameterTypeRule%
50+
PHPStan\Rules\Functions\IncompatibleClosureDefaultParameterTypeRule:
51+
phpstan.rules.rule: %featureToggles.closureDefaultParameterTypeRule%
4852
PHPStan\Rules\Methods\IllegalConstructorMethodCallRule:
4953
phpstan.rules.rule: %featureToggles.illegalConstructorMethodCall%
5054
PHPStan\Rules\Methods\IllegalConstructorStaticCallRule:
@@ -59,6 +63,10 @@ services:
5963
checkClassCaseSensitivity: %checkClassCaseSensitivity%
6064
tags:
6165
- phpstan.rules.rule
66+
-
67+
class: PHPStan\Rules\Functions\IncompatibleArrowFunctionDefaultParameterTypeRule
68+
-
69+
class: PHPStan\Rules\Functions\IncompatibleClosureDefaultParameterTypeRule
6270
-
6371
class: PHPStan\Rules\Functions\CallCallablesRule
6472
arguments:

conf/config.neon

+2
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ parameters:
5858
alwaysTrueAlwaysReported: false
5959
disableUnreachableBranchesRules: false
6060
varTagType: false
61+
closureDefaultParameterTypeRule: false
6162
fileExtensions:
6263
- php
6364
checkAdvancedIsset: false
@@ -283,6 +284,7 @@ parametersSchema:
283284
alwaysTrueAlwaysReported: bool()
284285
disableUnreachableBranchesRules: bool()
285286
varTagType: bool()
287+
closureDefaultParameterTypeRule: bool()
286288
])
287289
fileExtensions: listOf(string())
288290
checkAdvancedIsset: bool()

src/Analyser/NodeScopeResolver.php

+5-1
Original file line numberDiff line numberDiff line change
@@ -3271,7 +3271,11 @@ private function processArrowFunctionNode(
32713271
}
32723272

32733273
$arrowFunctionScope = $scope->enterArrowFunction($expr, $callableParameters);
3274-
$nodeCallback(new InArrowFunctionNode($expr), $arrowFunctionScope);
3274+
$arrowFunctionType = $arrowFunctionScope->getAnonymousFunctionReflection();
3275+
if (!$arrowFunctionType instanceof ClosureType) {
3276+
throw new ShouldNotHappenException();
3277+
}
3278+
$nodeCallback(new InArrowFunctionNode($arrowFunctionType, $expr), $arrowFunctionScope);
32753279
$this->processExprNode($expr->expr, $arrowFunctionScope, $nodeCallback, ExpressionContext::createTopLevel());
32763280

32773281
return new ExpressionResult($scope, false, []);

src/Node/InArrowFunctionNode.php

+7-1
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,25 @@
55
use PhpParser\Node;
66
use PhpParser\Node\Expr\ArrowFunction;
77
use PhpParser\NodeAbstract;
8+
use PHPStan\Type\ClosureType;
89

910
/** @api */
1011
class InArrowFunctionNode extends NodeAbstract implements VirtualNode
1112
{
1213

1314
private Node\Expr\ArrowFunction $originalNode;
1415

15-
public function __construct(ArrowFunction $originalNode)
16+
public function __construct(private ClosureType $closureType, ArrowFunction $originalNode)
1617
{
1718
parent::__construct($originalNode->getAttributes());
1819
$this->originalNode = $originalNode;
1920
}
2021

22+
public function getClosureType(): ClosureType
23+
{
24+
return $this->closureType;
25+
}
26+
2127
public function getOriginalNode(): Node\Expr\ArrowFunction
2228
{
2329
return $this->originalNode;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Functions;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Node\InArrowFunctionNode;
8+
use PHPStan\Rules\Rule;
9+
use PHPStan\Rules\RuleErrorBuilder;
10+
use PHPStan\ShouldNotHappenException;
11+
use PHPStan\Type\Generic\TemplateTypeHelper;
12+
use PHPStan\Type\VerbosityLevel;
13+
use function is_string;
14+
use function sprintf;
15+
16+
/**
17+
* @implements Rule<InArrowFunctionNode>
18+
*/
19+
class IncompatibleArrowFunctionDefaultParameterTypeRule implements Rule
20+
{
21+
22+
public function getNodeType(): string
23+
{
24+
return InArrowFunctionNode::class;
25+
}
26+
27+
public function processNode(Node $node, Scope $scope): array
28+
{
29+
$parameters = $node->getClosureType()->getParameters();
30+
31+
$errors = [];
32+
foreach ($node->getOriginalNode()->getParams() as $paramI => $param) {
33+
if ($param->default === null) {
34+
continue;
35+
}
36+
if (
37+
$param->var instanceof Node\Expr\Error
38+
|| !is_string($param->var->name)
39+
) {
40+
throw new ShouldNotHappenException();
41+
}
42+
43+
$defaultValueType = $scope->getType($param->default);
44+
$parameterType = $parameters[$paramI]->getType();
45+
$parameterType = TemplateTypeHelper::resolveToBounds($parameterType);
46+
47+
if ($parameterType->accepts($defaultValueType, true)->yes()) {
48+
continue;
49+
}
50+
51+
$verbosityLevel = VerbosityLevel::getRecommendedLevelByType($parameterType, $defaultValueType);
52+
53+
$errors[] = RuleErrorBuilder::message(sprintf(
54+
'Default value of the parameter #%d $%s (%s) of anonymous function is incompatible with type %s.',
55+
$paramI + 1,
56+
$param->var->name,
57+
$defaultValueType->describe($verbosityLevel),
58+
$parameterType->describe($verbosityLevel),
59+
))->line($param->getLine())->build();
60+
}
61+
62+
return $errors;
63+
}
64+
65+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Functions;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Node\InClosureNode;
8+
use PHPStan\Rules\Rule;
9+
use PHPStan\Rules\RuleErrorBuilder;
10+
use PHPStan\ShouldNotHappenException;
11+
use PHPStan\Type\Generic\TemplateTypeHelper;
12+
use PHPStan\Type\VerbosityLevel;
13+
use function is_string;
14+
use function sprintf;
15+
16+
/**
17+
* @implements Rule<InClosureNode>
18+
*/
19+
class IncompatibleClosureDefaultParameterTypeRule implements Rule
20+
{
21+
22+
public function getNodeType(): string
23+
{
24+
return InClosureNode::class;
25+
}
26+
27+
public function processNode(Node $node, Scope $scope): array
28+
{
29+
$parameters = $node->getClosureType()->getParameters();
30+
31+
$errors = [];
32+
foreach ($node->getOriginalNode()->getParams() as $paramI => $param) {
33+
if ($param->default === null) {
34+
continue;
35+
}
36+
if (
37+
$param->var instanceof Node\Expr\Error
38+
|| !is_string($param->var->name)
39+
) {
40+
throw new ShouldNotHappenException();
41+
}
42+
43+
$defaultValueType = $scope->getType($param->default);
44+
$parameterType = $parameters[$paramI]->getType();
45+
$parameterType = TemplateTypeHelper::resolveToBounds($parameterType);
46+
47+
if ($parameterType->accepts($defaultValueType, true)->yes()) {
48+
continue;
49+
}
50+
51+
$verbosityLevel = VerbosityLevel::getRecommendedLevelByType($parameterType, $defaultValueType);
52+
53+
$errors[] = RuleErrorBuilder::message(sprintf(
54+
'Default value of the parameter #%d $%s (%s) of anonymous function is incompatible with type %s.',
55+
$paramI + 1,
56+
$param->var->name,
57+
$defaultValueType->describe($verbosityLevel),
58+
$parameterType->describe($verbosityLevel),
59+
))->line($param->getLine())->build();
60+
}
61+
62+
return $errors;
63+
}
64+
65+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Functions;
4+
5+
use PHPStan\Rules\Rule;
6+
use PHPStan\Testing\RuleTestCase;
7+
use const PHP_VERSION_ID;
8+
9+
/**
10+
* @extends RuleTestCase<IncompatibleArrowFunctionDefaultParameterTypeRule>
11+
*/
12+
class IncompatibleArrowFunctionDefaultParameterTypeRuleTest extends RuleTestCase
13+
{
14+
15+
protected function getRule(): Rule
16+
{
17+
return new IncompatibleArrowFunctionDefaultParameterTypeRule();
18+
}
19+
20+
public function testRule(): void
21+
{
22+
if (PHP_VERSION_ID < 70400) {
23+
$this->markTestSkipped('Test requires PHP 7.4.');
24+
}
25+
$this->analyse([__DIR__ . '/data/incompatible-default-parameter-type-arrow-functions.php'], [
26+
[
27+
'Default value of the parameter #1 $i (string) of anonymous function is incompatible with type int.',
28+
13,
29+
],
30+
]);
31+
}
32+
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Functions;
4+
5+
use PHPStan\Rules\Rule;
6+
use PHPStan\Testing\RuleTestCase;
7+
use const PHP_VERSION_ID;
8+
9+
/**
10+
* @extends RuleTestCase<IncompatibleClosureDefaultParameterTypeRule>
11+
*/
12+
class IncompatibleClosureFunctionDefaultParameterTypeRuleTest extends RuleTestCase
13+
{
14+
15+
protected function getRule(): Rule
16+
{
17+
return new IncompatibleClosureDefaultParameterTypeRule();
18+
}
19+
20+
public function testRule(): void
21+
{
22+
if (PHP_VERSION_ID < 70400) {
23+
$this->markTestSkipped('Test requires PHP 7.4.');
24+
}
25+
$this->analyse([__DIR__ . '/data/incompatible-default-parameter-type-closure.php'], [
26+
[
27+
'Default value of the parameter #1 $i (string) of anonymous function is incompatible with type int.',
28+
19,
29+
],
30+
]);
31+
}
32+
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php // lint >= 7.4
2+
3+
namespace IncompatibleArrowFunctionDefaultParameterType;
4+
5+
class Foo
6+
{
7+
8+
public function doFoo(): void
9+
{
10+
$f = fn (int $i = null) => '1';
11+
$g = fn (?int $i = null) => '1';
12+
$h = fn (int $i = 5) => '1';
13+
$i = fn (int $i = 'foo') => '1';
14+
}
15+
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php // lint >= 7.4
2+
3+
namespace IncompatibleClosureDefaultParameterType;
4+
5+
class Foo
6+
{
7+
8+
public function doFoo(): void
9+
{
10+
$f = function (int $i = null) {
11+
return '1';
12+
};
13+
$g = function (?int $i = null) {
14+
return '1';
15+
};
16+
$h = function (int $i = 5) {
17+
return '1';
18+
};
19+
$i = function (int $i = 'foo') {
20+
return '1';
21+
};
22+
}
23+
24+
}

0 commit comments

Comments
 (0)