Skip to content

Commit

Permalink
Rules to accomodate first-class callables
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Nov 18, 2021
1 parent 23ea5ed commit e12d55f
Show file tree
Hide file tree
Showing 11 changed files with 322 additions and 1 deletion.
2 changes: 1 addition & 1 deletion composer.json
Expand Up @@ -7,7 +7,7 @@
],
"require": {
"php": "^7.1 || ^8.0",
"phpstan/phpstan": "^1.0"
"phpstan/phpstan": "^1.2.0"
},
"require-dev": {
"nikic/php-parser": "^4.13.0",
Expand Down
3 changes: 3 additions & 0 deletions rules.neon
Expand Up @@ -38,10 +38,13 @@ rules:
- PHPStan\Rules\Operators\OperandsInArithmeticMultiplicationRule
- PHPStan\Rules\Operators\OperandsInArithmeticSubtractionRule
- PHPStan\Rules\StrictCalls\DynamicCallOnStaticMethodsRule
- PHPStan\Rules\StrictCalls\DynamicCallOnStaticMethodsCallableRule
- PHPStan\Rules\StrictCalls\StrictFunctionCallsRule
- PHPStan\Rules\SwitchConditions\MatchingTypeInSwitchCaseConditionRule
- PHPStan\Rules\VariableVariables\VariableMethodCallRule
- PHPStan\Rules\VariableVariables\VariableMethodCallableRule
- PHPStan\Rules\VariableVariables\VariableStaticMethodCallRule
- PHPStan\Rules\VariableVariables\VariableStaticMethodCallableRule
- PHPStan\Rules\VariableVariables\VariableStaticPropertyFetchRule
- PHPStan\Rules\VariableVariables\VariableVariablesRule

Expand Down
63 changes: 63 additions & 0 deletions src/Rules/StrictCalls/DynamicCallOnStaticMethodsCallableRule.php
@@ -0,0 +1,63 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\StrictCalls;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Node\MethodCallableNode;
use PHPStan\Rules\RuleLevelHelper;
use PHPStan\Type\ErrorType;
use PHPStan\Type\Type;

/**
* @implements \PHPStan\Rules\Rule<MethodCallableNode>
*/
class DynamicCallOnStaticMethodsCallableRule implements \PHPStan\Rules\Rule
{

/** @var \PHPStan\Rules\RuleLevelHelper */
private $ruleLevelHelper;

public function __construct(RuleLevelHelper $ruleLevelHelper)
{
$this->ruleLevelHelper = $ruleLevelHelper;
}

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

public function processNode(Node $node, Scope $scope): array
{
if (!$node->getName() instanceof Node\Identifier) {
return [];
}

$name = $node->getName()->name;
$type = $this->ruleLevelHelper->findTypeToCheck(
$scope,
$node->getVar(),
'',
function (Type $type) use ($name): bool {
return $type->canCallMethods()->yes() && $type->hasMethod($name)->yes();
}
)->getType();

if ($type instanceof ErrorType || !$type->canCallMethods()->yes() || !$type->hasMethod($name)->yes()) {
return [];
}

$methodReflection = $type->getMethod($name, $scope);
if ($methodReflection->isStatic()) {
return [sprintf(
'Dynamic call to static method %s::%s().',
$methodReflection->getDeclaringClass()->getDisplayName(),
$methodReflection->getName()
)];
}

return [];
}

}
36 changes: 36 additions & 0 deletions src/Rules/VariableVariables/VariableMethodCallableRule.php
@@ -0,0 +1,36 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\VariableVariables;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Node\MethodCallableNode;
use PHPStan\Rules\Rule;
use PHPStan\Type\VerbosityLevel;

/**
* @implements Rule<MethodCallableNode>
*/
class VariableMethodCallableRule implements Rule
{

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

public function processNode(Node $node, Scope $scope): array
{
if ($node->getName() instanceof Node\Identifier) {
return [];
}

return [
sprintf(
'Variable method call on %s.',
$scope->getType($node->getVar())->describe(VerbosityLevel::typeOnly())
),
];
}

}
42 changes: 42 additions & 0 deletions src/Rules/VariableVariables/VariableStaticMethodCallableRule.php
@@ -0,0 +1,42 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\VariableVariables;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Node\StaticMethodCallableNode;
use PHPStan\Rules\Rule;
use PHPStan\Type\VerbosityLevel;

/**
* @implements Rule<StaticMethodCallableNode>
*/
class VariableStaticMethodCallableRule implements Rule
{

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

public function processNode(Node $node, Scope $scope): array
{
if ($node->getName() instanceof Node\Identifier) {
return [];
}

if ($node->getClass() instanceof Node\Name) {
$methodCalledOn = $scope->resolveName($node->getClass());
} else {
$methodCalledOn = $scope->getType($node->getClass())->describe(VerbosityLevel::typeOnly());
}

return [
sprintf(
'Variable static method call on %s.',
$methodCalledOn
),
];
}

}
@@ -0,0 +1,45 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\StrictCalls;

use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleLevelHelper;

/**
* @extends \PHPStan\Testing\RuleTestCase<DynamicCallOnStaticMethodsCallableRule>
*/
class DynamicCallOnStaticMethodsCallableRuleTest extends \PHPStan\Testing\RuleTestCase
{

protected function getRule(): Rule
{
return new DynamicCallOnStaticMethodsCallableRule(self::getContainer()->getByType(RuleLevelHelper::class));
}

public function testRule(): void
{
if (PHP_VERSION_ID < 80100) {
self::markTestSkipped('Test requires PHP 8.1.');
}

$this->analyse([__DIR__ . '/data/dynamic-calls-on-static-methods-callables.php'], [
[
'Dynamic call to static method StrictCallsCallables\ClassWithStaticMethod::foo().',
14,
],
[
'Dynamic call to static method StrictCallsCallables\ClassWithStaticMethod::foo().',
21,
],
[
'Dynamic call to static method StrictCallsCallables\ClassUsingTrait::foo().',
34,
],
[
'Dynamic call to static method StrictCallsCallables\ClassUsingTrait::foo().',
46,
],
]);
}

}
@@ -0,0 +1,48 @@
<?php // lint >= 8.1

namespace StrictCallsCallables;

class ClassWithStaticMethod
{
public static function foo()
{

}

public function bar()
{
$this->foo(...);
$this->bar(...);
}
}

function () {
$classWithStaticMethod = new ClassWithStaticMethod();
$classWithStaticMethod->foo(...);
$classWithStaticMethod->bar(...);
};

trait TraitWithStaticMethod
{
public static function foo()
{

}

public function bar()
{
$this->foo(...);
$this->bar(...);
}
}

class ClassUsingTrait
{
use TraitWithStaticMethod;
}

function () {
$classUsingTrait = new ClassUsingTrait();
$classUsingTrait->foo(...);
$classUsingTrait->bar(...);
};
33 changes: 33 additions & 0 deletions tests/Rules/VariableVariables/VariableMethodCallableRuleTest.php
@@ -0,0 +1,33 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\VariableVariables;

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

/**
* @extends RuleTestCase<VariableMethodCallableRule>
*/
class VariableMethodCallableRuleTest extends RuleTestCase
{

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

public function testRule(): void
{
if (PHP_VERSION_ID < 80100) {
self::markTestSkipped('Test requires PHP 8.1.');
}

$this->analyse([__DIR__ . '/data/methods-callables.php'], [
[
'Variable method call on stdClass.',
7,
],
]);
}

}
@@ -0,0 +1,33 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\VariableVariables;

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

/**
* @extends RuleTestCase<VariableStaticMethodCallableRule>
*/
class VariableStaticMethodCallableRuleTest extends RuleTestCase
{

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

public function testRule(): void
{
$this->analyse([__DIR__ . '/data/staticMethods-callables.php'], [
[
'Variable static method call on Foo.',
7,
],
[
'Variable static method call on stdClass.',
9,
],
]);
}

}
8 changes: 8 additions & 0 deletions tests/Rules/VariableVariables/data/methods-callables.php
@@ -0,0 +1,8 @@
<?php // lint >= 8.1

function (stdClass $std) {
$std->foo(...);

$foo = 'bar';
$std->$foo(...);
};
10 changes: 10 additions & 0 deletions tests/Rules/VariableVariables/data/staticMethods-callables.php
@@ -0,0 +1,10 @@
<?php // lint >= 8.1

function (stdClass $std) {
Foo::doFoo(...);

$foo = 'doBar';
Foo::$foo(...);

$std::$foo(...);
};

0 comments on commit e12d55f

Please sign in to comment.