Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,11 @@ services:
class: PMarki\PHPStanRules\Rules\ProtectedPropertyRule
tags:
- phpstan.rules.rule
-
class: PMarki\PHPStanRules\Rules\DuplicatedArrayKeys
tags:
- phpstan.rules.rule
-
class: PMarki\PHPStanRules\Rules\BooleanArgumentFlagRule
tags:
- phpstan.rules.rule
118 changes: 118 additions & 0 deletions src/Rules/BooleanArgumentFlagRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?php

declare(strict_types=1);

namespace PMarki\PHPStanRules\Rules;

use PhpParser\Node;
use PhpParser\Node\FunctionLike;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\FileTypeMapper;
use PMarki\PHPStanRules\Utils\Ancestors;

/**
* @implements Rule<FunctionLike>
*/
class BooleanArgumentFlagRule implements Rule
{
public function __construct(
private readonly Ancestors $ancestors,
private readonly FileTypeMapper $fileTypeMapper,
) {
}

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

/**
* @return array<int, \PHPStan\Rules\RuleError>
*/
public function processNode(Node $node, Scope $scope): array
{
if (!$node instanceof FunctionLike) {
return [];
}

if ($node->name instanceof Node\Identifier && $this->ancestors->methodInherited($node->name->name, $scope)) {
return [];
}

$errors = [];

foreach ($node->getParams() as $param) {
if (!$param->type instanceof Node\Identifier || !$param->var instanceof Node\Expr\Variable) {
continue;
}
$argName = $param->var->name;

if ($param->type->toString() === 'bool') {
$errors[] = RuleErrorBuilder::message(
"Boolean flag '\${$argName}' indicates violation of Single Responsibility Principle."
)->tip('Extract the logic in the boolean flag into its own class or method.')
->identifier('extraBooleanArgumentFlag')
->build();
}
}

foreach ($this->getTypeFromDocBlock($scope, $node) as $paramName) {
$errors[] = RuleErrorBuilder::message(
"Boolean flag '{$paramName}' indicates violation of Single Responsibility Principle."
)->tip('Extract the logic in the boolean flag into its own class or method.')
->identifier('extraBooleanArgumentFlag')
->build();
}


return $errors;
}

private function getTypeFromDocBlock(Scope $scope, Node $node): array
{
if ($node->getDocComment() === null) {
return [];
}

$resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
$scope->getFile(),
$scope->isInClass() ? $scope->getClassReflection()->getName() : null,
$scope->isInTrait() ? $scope->getTraitReflection()->getName() : null,
$scope->getFunctionName(),
$node->getDocComment()->getText(),
);

$docNodes = $resolvedPhpDoc->getPhpDocNodes();
$types = [];
foreach ($docNodes as $docNode) {
$params = $docNode->getTagsByName('@param');
foreach ($params as $param) {
if ($param->value->type->name === 'bool') {
$types[] = $param->value->parameterName;
}
}
}

return $types;



$docNodes = $resolvedPhpDoc->getParamTags();
if ($docNodes === false) {
return [];
}

$types = [];
/** @var \PHPStan\PhpDoc\Tag\ParamTag $docNode */
foreach ($docNodes as $docNode) {
$type = $docNode->getType();
if (!$type->isBoolean()->yes()) {
continue;
}
}

return $types;
}
}
30 changes: 30 additions & 0 deletions src/Utils/Ancestors.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace PMarki\PHPStanRules\Utils;

use PHPStan\Analyser\Scope;

class Ancestors
{
public function methodInherited(string $methodName, Scope $scope): bool
{
$classReflection = $scope->getClassReflection();
if (!$scope->isInClass()) {
return false;
}

foreach (\array_merge($classReflection->getParents(), $classReflection->getInterfaces()) as $ancestor) {
if ($ancestor->hasMethod($methodName)) {
$method = $ancestor->getMethod($methodName, $scope);

if (!$method->isPrivate()) {
return true;
}
}
}

return false;
}
}
55 changes: 55 additions & 0 deletions tests/BooleanArgumentFlagRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

namespace PMarki\PHPStanRules\Tests;

use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
use PHPStan\Type\FileTypeMapper;
use PMarki\PHPStanRules\Rules\BooleanArgumentFlagRule;
use PMarki\PHPStanRules\Utils\Ancestors;

class BooleanArgumentFlagRuleTest extends RuleTestCase
{
private const TIP = "\n 💡 Extract the logic in the boolean flag into its own class or method.";

public function testBooleanArgumentFlag(): void
{
$this->analyse(
[
__DIR__ . '/fixtures/BooleanArgumentFlagRule.php',
],
[
[
"Boolean flag '\$bool' indicates violation of Single Responsibility Principle." . self::TIP,
5,
],
[
"Boolean flag '\$bool1' indicates violation of Single Responsibility Principle." . self::TIP,
9,
],
[
"Boolean flag '\$bool2' indicates violation of Single Responsibility Principle." . self::TIP,
9,
],
[
"Boolean flag '\$bool' indicates violation of Single Responsibility Principle." . self::TIP,
19
],
[
"Boolean flag '\$bool' indicates violation of Single Responsibility Principle." . self::TIP,
32,
],
],
);
}

protected function getRule(): Rule
{
return new BooleanArgumentFlagRule(
new Ancestors(),
self::getContainer()->getByType(FileTypeMapper::class)
);
}
}
39 changes: 39 additions & 0 deletions tests/fixtures/BooleanArgumentFlagRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php declare(strict_types=1);

namespace BooleanArgumentFlagRule;

function boolFlag(int $int, bool $bool)
{
}

function boolDoubleFlag(int $int, bool $bool1, bool $bool2)
{
}

function noBoolFlag(int $int, string $string)
{
}

class BooleanArgumentFlagRule
{
public function boolFlag(int $int, bool $bool)
{
}

private function noBoolFlag(int $int, string $string)
{
}

/**
* @param int $int
* @param bool $bool
* @return void
*/
public function boolFlagNoType($int, $bool)
{
}

public function boolUnionFlag(int $int, bool|int $bool)
{
}
}