Skip to content

Commit

Permalink
Bleeding edge - IncompatibleDefaultParameterTypeRule for closures
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Jan 25, 2023
1 parent c4ee0b8 commit 0264f5b
Show file tree
Hide file tree
Showing 11 changed files with 259 additions and 2 deletions.
1 change: 1 addition & 0 deletions conf/bleedingEdge.neon
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ parameters:
alwaysTrueAlwaysReported: true
disableUnreachableBranchesRules: true
varTagType: true
closureDefaultParameterTypeRule: true
8 changes: 8 additions & 0 deletions conf/config.level2.neon
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ rules:
- PHPStan\Rules\Properties\AccessPrivatePropertyThroughStaticRule

conditionalTags:
PHPStan\Rules\Functions\IncompatibleArrowFunctionDefaultParameterTypeRule:
phpstan.rules.rule: %featureToggles.closureDefaultParameterTypeRule%
PHPStan\Rules\Functions\IncompatibleClosureDefaultParameterTypeRule:
phpstan.rules.rule: %featureToggles.closureDefaultParameterTypeRule%
PHPStan\Rules\Methods\IllegalConstructorMethodCallRule:
phpstan.rules.rule: %featureToggles.illegalConstructorMethodCall%
PHPStan\Rules\Methods\IllegalConstructorStaticCallRule:
Expand All @@ -59,6 +63,10 @@ services:
checkClassCaseSensitivity: %checkClassCaseSensitivity%
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Functions\IncompatibleArrowFunctionDefaultParameterTypeRule
-
class: PHPStan\Rules\Functions\IncompatibleClosureDefaultParameterTypeRule
-
class: PHPStan\Rules\Functions\CallCallablesRule
arguments:
Expand Down
2 changes: 2 additions & 0 deletions conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ parameters:
alwaysTrueAlwaysReported: false
disableUnreachableBranchesRules: false
varTagType: false
closureDefaultParameterTypeRule: false
fileExtensions:
- php
checkAdvancedIsset: false
Expand Down Expand Up @@ -283,6 +284,7 @@ parametersSchema:
alwaysTrueAlwaysReported: bool()
disableUnreachableBranchesRules: bool()
varTagType: bool()
closureDefaultParameterTypeRule: bool()
])
fileExtensions: listOf(string())
checkAdvancedIsset: bool()
Expand Down
6 changes: 5 additions & 1 deletion src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -3271,7 +3271,11 @@ private function processArrowFunctionNode(
}

$arrowFunctionScope = $scope->enterArrowFunction($expr, $callableParameters);
$nodeCallback(new InArrowFunctionNode($expr), $arrowFunctionScope);
$arrowFunctionType = $arrowFunctionScope->getAnonymousFunctionReflection();
if (!$arrowFunctionType instanceof ClosureType) {
throw new ShouldNotHappenException();
}
$nodeCallback(new InArrowFunctionNode($arrowFunctionType, $expr), $arrowFunctionScope);
$this->processExprNode($expr->expr, $arrowFunctionScope, $nodeCallback, ExpressionContext::createTopLevel());

return new ExpressionResult($scope, false, []);
Expand Down
8 changes: 7 additions & 1 deletion src/Node/InArrowFunctionNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,25 @@
use PhpParser\Node;
use PhpParser\Node\Expr\ArrowFunction;
use PhpParser\NodeAbstract;
use PHPStan\Type\ClosureType;

/** @api */
class InArrowFunctionNode extends NodeAbstract implements VirtualNode
{

private Node\Expr\ArrowFunction $originalNode;

public function __construct(ArrowFunction $originalNode)
public function __construct(private ClosureType $closureType, ArrowFunction $originalNode)
{
parent::__construct($originalNode->getAttributes());
$this->originalNode = $originalNode;
}

public function getClosureType(): ClosureType
{
return $this->closureType;
}

public function getOriginalNode(): Node\Expr\ArrowFunction
{
return $this->originalNode;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Functions;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Node\InArrowFunctionNode;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\Generic\TemplateTypeHelper;
use PHPStan\Type\VerbosityLevel;
use function is_string;
use function sprintf;

/**
* @implements Rule<InArrowFunctionNode>
*/
class IncompatibleArrowFunctionDefaultParameterTypeRule implements Rule
{

public function getNodeType(): string
{
return InArrowFunctionNode::class;
}

public function processNode(Node $node, Scope $scope): array
{
$parameters = $node->getClosureType()->getParameters();

$errors = [];
foreach ($node->getOriginalNode()->getParams() as $paramI => $param) {
if ($param->default === null) {
continue;
}
if (
$param->var instanceof Node\Expr\Error
|| !is_string($param->var->name)
) {
throw new ShouldNotHappenException();
}

$defaultValueType = $scope->getType($param->default);
$parameterType = $parameters[$paramI]->getType();
$parameterType = TemplateTypeHelper::resolveToBounds($parameterType);

if ($parameterType->accepts($defaultValueType, true)->yes()) {
continue;
}

$verbosityLevel = VerbosityLevel::getRecommendedLevelByType($parameterType, $defaultValueType);

$errors[] = RuleErrorBuilder::message(sprintf(
'Default value of the parameter #%d $%s (%s) of anonymous function is incompatible with type %s.',
$paramI + 1,
$param->var->name,
$defaultValueType->describe($verbosityLevel),
$parameterType->describe($verbosityLevel),
))->line($param->getLine())->build();
}

return $errors;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Functions;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Node\InClosureNode;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\Generic\TemplateTypeHelper;
use PHPStan\Type\VerbosityLevel;
use function is_string;
use function sprintf;

/**
* @implements Rule<InClosureNode>
*/
class IncompatibleClosureDefaultParameterTypeRule implements Rule
{

public function getNodeType(): string
{
return InClosureNode::class;
}

public function processNode(Node $node, Scope $scope): array
{
$parameters = $node->getClosureType()->getParameters();

$errors = [];
foreach ($node->getOriginalNode()->getParams() as $paramI => $param) {
if ($param->default === null) {
continue;
}
if (
$param->var instanceof Node\Expr\Error
|| !is_string($param->var->name)
) {
throw new ShouldNotHappenException();
}

$defaultValueType = $scope->getType($param->default);
$parameterType = $parameters[$paramI]->getType();
$parameterType = TemplateTypeHelper::resolveToBounds($parameterType);

if ($parameterType->accepts($defaultValueType, true)->yes()) {
continue;
}

$verbosityLevel = VerbosityLevel::getRecommendedLevelByType($parameterType, $defaultValueType);

$errors[] = RuleErrorBuilder::message(sprintf(
'Default value of the parameter #%d $%s (%s) of anonymous function is incompatible with type %s.',
$paramI + 1,
$param->var->name,
$defaultValueType->describe($verbosityLevel),
$parameterType->describe($verbosityLevel),
))->line($param->getLine())->build();
}

return $errors;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Functions;

use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
use const PHP_VERSION_ID;

/**
* @extends RuleTestCase<IncompatibleArrowFunctionDefaultParameterTypeRule>
*/
class IncompatibleArrowFunctionDefaultParameterTypeRuleTest extends RuleTestCase
{

protected function getRule(): Rule
{
return new IncompatibleArrowFunctionDefaultParameterTypeRule();
}

public function testRule(): void
{
if (PHP_VERSION_ID < 70400) {
$this->markTestSkipped('Test requires PHP 7.4.');
}
$this->analyse([__DIR__ . '/data/incompatible-default-parameter-type-arrow-functions.php'], [
[
'Default value of the parameter #1 $i (string) of anonymous function is incompatible with type int.',
13,
],
]);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Functions;

use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
use const PHP_VERSION_ID;

/**
* @extends RuleTestCase<IncompatibleClosureDefaultParameterTypeRule>
*/
class IncompatibleClosureFunctionDefaultParameterTypeRuleTest extends RuleTestCase
{

protected function getRule(): Rule
{
return new IncompatibleClosureDefaultParameterTypeRule();
}

public function testRule(): void
{
if (PHP_VERSION_ID < 70400) {
$this->markTestSkipped('Test requires PHP 7.4.');
}
$this->analyse([__DIR__ . '/data/incompatible-default-parameter-type-closure.php'], [
[
'Default value of the parameter #1 $i (string) of anonymous function is incompatible with type int.',
19,
],
]);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php // lint >= 7.4

namespace IncompatibleArrowFunctionDefaultParameterType;

class Foo
{

public function doFoo(): void
{
$f = fn (int $i = null) => '1';
$g = fn (?int $i = null) => '1';
$h = fn (int $i = 5) => '1';
$i = fn (int $i = 'foo') => '1';
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php // lint >= 7.4

namespace IncompatibleClosureDefaultParameterType;

class Foo
{

public function doFoo(): void
{
$f = function (int $i = null) {
return '1';
};
$g = function (?int $i = null) {
return '1';
};
$h = function (int $i = 5) {
return '1';
};
$i = function (int $i = 'foo') {
return '1';
};
}

}

0 comments on commit 0264f5b

Please sign in to comment.