-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
55ec769
commit 65dbf5e
Showing
12 changed files
with
478 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace TomasVotruba\Handyman\Enum; | ||
|
||
final class ClassName | ||
{ | ||
/** | ||
* @var string | ||
*/ | ||
public const EVENT_DISPATCHER_INTERFACE = 'Symfony\Component\EventDispatcher\EventDispatcherInterface'; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace TomasVotruba\Handyman\PHPStan\Rule; | ||
|
||
use PhpParser\Node; | ||
use PhpParser\Node\Expr\StaticCall; | ||
use PhpParser\Node\Identifier; | ||
use PhpParser\Node\Name; | ||
use PhpParser\Node\Stmt\ClassMethod; | ||
use PhpParser\NodeFinder; | ||
use PHPStan\Analyser\Scope; | ||
use PHPStan\Reflection\ClassReflection; | ||
use PHPStan\Rules\Rule; | ||
use PHPStan\Rules\RuleError; | ||
use PHPStan\Rules\RuleErrorBuilder; | ||
|
||
/** | ||
* @implements Rule<ClassMethod> | ||
*/ | ||
final class NoConstructorOverrideRule implements Rule | ||
{ | ||
/** | ||
* @var string | ||
*/ | ||
public const ERROR_MESSAGE = 'Possible __construct() override, this can cause missing dependencies or setup'; | ||
|
||
/** | ||
* @var string | ||
*/ | ||
private const CONSTRUCTOR_NAME = '__construct'; | ||
|
||
public function getNodeType(): string | ||
{ | ||
return ClassMethod::class; | ||
} | ||
|
||
/** | ||
* @param ClassMethod $node | ||
* @return RuleError[] | ||
*/ | ||
public function processNode(Node $node, Scope $scope): array | ||
{ | ||
if ($node->name->toLowerString() !== self::CONSTRUCTOR_NAME) { | ||
return []; | ||
} | ||
|
||
// has parent constructor call? | ||
if (! $scope->isInClass()) { | ||
return []; | ||
} | ||
|
||
// parent has no cunstructor, we can skip it | ||
$classReflection = $scope->getClassReflection(); | ||
if ($classReflection->isAnonymous()) { | ||
return []; | ||
} | ||
|
||
$parentClassReflection = $classReflection->getParentClass(); | ||
|
||
// no parent class? let it go | ||
if (! $parentClassReflection instanceof ClassReflection) { | ||
return []; | ||
} | ||
|
||
if (! $parentClassReflection->hasConstructor()) { | ||
return []; | ||
} | ||
|
||
$nodeFinder = new NodeFinder(); | ||
$parentConstructorStaticCall = $nodeFinder->findFirst($node->stmts, function (Node $node): bool { | ||
if (! $node instanceof StaticCall) { | ||
return false; | ||
} | ||
|
||
if (! $node->class instanceof Name) { | ||
return false; | ||
} | ||
|
||
if (! $node->name instanceof Identifier) { | ||
return false; | ||
} | ||
|
||
return $node->name->toString() === '__construct'; | ||
}); | ||
|
||
if ($parentConstructorStaticCall instanceof StaticCall) { | ||
return []; | ||
} | ||
|
||
$ruleError = RuleErrorBuilder::message(self::ERROR_MESSAGE) | ||
->build(); | ||
|
||
return [$ruleError]; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace TomasVotruba\Handyman\PHPStan\Rule; | ||
|
||
use PhpParser\Node; | ||
use PhpParser\Node\Expr\MethodCall; | ||
use PhpParser\Node\Identifier; | ||
use PHPStan\Analyser\Scope; | ||
use PHPStan\Rules\Rule; | ||
|
||
final class NoDocumentMockingRule implements Rule | ||
{ | ||
/** | ||
* @var string | ||
*/ | ||
public const ERROR_MESSAGE = 'Instead of document mocking, create object directly to get better type support'; | ||
|
||
public function getNodeType(): string | ||
{ | ||
return MethodCall::class; | ||
} | ||
|
||
/** | ||
* @param MethodCall $node | ||
* @return string[] | ||
*/ | ||
public function processNode(Node $node, Scope $scope): array | ||
{ | ||
if ($node->isFirstClassCallable()) { | ||
return []; | ||
} | ||
|
||
if (! $node->name instanceof Identifier) { | ||
return []; | ||
} | ||
|
||
$methodName = $node->name->toString(); | ||
if ($methodName !== 'createMock') { | ||
return []; | ||
} | ||
|
||
$firstArg = $node->getArgs()[0]; | ||
$mockedClassType = $scope->getType($firstArg->value); | ||
foreach ($mockedClassType->getConstantStrings() as $constantString) { | ||
if (! str_contains($constantString->getValue(), '\\Document\\')) { | ||
continue; | ||
} | ||
|
||
return [self::ERROR_MESSAGE]; | ||
} | ||
|
||
return []; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace TomasVotruba\Handyman\PHPStan\Rule; | ||
|
||
use PhpParser\Node; | ||
use PhpParser\Node\Stmt\Class_; | ||
use PHPStan\Analyser\Scope; | ||
use PHPStan\Node\InClassNode; | ||
use PHPStan\Rules\Rule; | ||
use PHPStan\Rules\RuleError; | ||
use PHPStan\Rules\RuleErrorBuilder; | ||
|
||
/** | ||
* Based on https://tomasvotruba.com/blog/2019/07/22/how-to-convert-listeners-to-subscribers-and-reduce-your-configs | ||
* Subscribers have much better PHP support - IDE, PHPStan + Rector - than simple yaml files | ||
* | ||
* @implements Rule<InClassNode> | ||
*/ | ||
final class NoListenerWithoutContractRule implements Rule | ||
{ | ||
/** | ||
* @var string | ||
*/ | ||
public const ERROR_MESSAGE = 'There should be no listeners defined in yaml config, use contract + PHP instead'; | ||
|
||
public function getNodeType(): string | ||
{ | ||
return InClassNode::class; | ||
} | ||
|
||
/** | ||
* @param InClassNode $node | ||
* @return RuleError[] | ||
*/ | ||
public function processNode(Node $node, Scope $scope): array | ||
{ | ||
if (! $scope->isInClass()) { | ||
return []; | ||
} | ||
|
||
$classReflection = $scope->getClassReflection(); | ||
if (! str_ends_with($classReflection->getName(), 'Listener')) { | ||
return []; | ||
} | ||
|
||
$class = $node->getOriginalNode(); | ||
if (! $class instanceof Class_) { | ||
return []; | ||
} | ||
|
||
if ($class->implements !== []) { | ||
return []; | ||
} | ||
|
||
$ruleError = RuleErrorBuilder::message(self::ERROR_MESSAGE)->build(); | ||
|
||
return [$ruleError]; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace TomasVotruba\Handyman\PHPStan\Rule; | ||
|
||
use PhpParser\Node; | ||
use PhpParser\Node\Expr; | ||
use PhpParser\Node\Expr\ArrayItem; | ||
use PhpParser\Node\Expr\ClassConstFetch; | ||
use PhpParser\Node\Stmt\ClassMethod; | ||
use PhpParser\NodeFinder; | ||
use PHPStan\Analyser\Scope; | ||
use PHPStan\Rules\Rule; | ||
use PHPStan\Rules\RuleError; | ||
use PHPStan\Rules\RuleErrorBuilder; | ||
use Symfony\Component\Form\FormEvents; | ||
|
||
/** | ||
* @implements Rule<ClassMethod> | ||
*/ | ||
final class NoStringInGetSubscribedEventsRule implements Rule | ||
{ | ||
/** | ||
* @var string | ||
*/ | ||
private const EVENT_SUBSCRIBER_INTERFACE = 'Symfony\Component\EventDispatcher\EventSubscriberInterface'; | ||
|
||
/** | ||
* @var string | ||
*/ | ||
private const ERROR_MESSAGE = 'Symfony getSubscribedEvents() method must contain only event class references, no strings'; | ||
|
||
public function getNodeType(): string | ||
{ | ||
return ClassMethod::class; | ||
} | ||
|
||
/** | ||
* @param ClassMethod $node | ||
* @return RuleError[] | ||
*/ | ||
public function processNode(Node $node, Scope $scope): array | ||
{ | ||
if ($node->name->toString() !== 'getSubscribedEvents') { | ||
return []; | ||
} | ||
|
||
$classReflection = $scope->getClassReflection(); | ||
|
||
// only handle symfony one | ||
if (! $classReflection->implementsInterface(self::EVENT_SUBSCRIBER_INTERFACE)) { | ||
return []; | ||
} | ||
|
||
$nodeFinder = new NodeFinder(); | ||
|
||
/** @var ArrayItem[] $arrayItems */ | ||
$arrayItems = $nodeFinder->findInstanceOf($node->stmts, ArrayItem::class); | ||
|
||
foreach ($arrayItems as $arrayItem) { | ||
if (! $arrayItem->key instanceof Expr) { | ||
continue; | ||
} | ||
|
||
// must be class const fetch | ||
if ($arrayItem->key instanceof ClassConstFetch) { | ||
$classConstFetch = $arrayItem->key; | ||
|
||
// skip Symfony FormEvents::class | ||
if ($classConstFetch->class->toString() === FormEvents::class) { | ||
Check failure on line 71 in src/PHPStan/Rule/NoStringInGetSubscribedEventsRule.php GitHub Actions / PHPStan
|
||
continue; | ||
} | ||
|
||
if ($classConstFetch->name->toString() === 'class') { | ||
continue; | ||
} | ||
|
||
continue; | ||
} | ||
|
||
$ruleError = RuleErrorBuilder::message(self::ERROR_MESSAGE)->build(); | ||
return [$ruleError]; | ||
} | ||
|
||
return []; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace TomasVotruba\Handyman\PHPStan\Rule; | ||
|
||
use PhpParser\Node; | ||
use PhpParser\Node\Expr\MethodCall; | ||
use PhpParser\Node\Identifier; | ||
use PHPStan\Analyser\Scope; | ||
use PHPStan\Rules\Rule; | ||
use PHPStan\Rules\RuleError; | ||
use PHPStan\Rules\RuleErrorBuilder; | ||
use PHPStan\Type\ObjectType; | ||
use TomasVotruba\Handyman\Enum\ClassName; | ||
|
||
final class SingleArgEventDispatchRule implements Rule | ||
{ | ||
/** | ||
* @var string | ||
*/ | ||
public const ERROR_MESSAGE = 'The event dispatch() method can have only 1 arg - the event object'; | ||
|
||
public function getNodeType(): string | ||
{ | ||
return MethodCall::class; | ||
} | ||
|
||
/** | ||
* @param MethodCall $node | ||
* @return RuleError[] | ||
*/ | ||
public function processNode(Node $node, Scope $scope): array | ||
{ | ||
if (! $node->name instanceof Identifier) { | ||
return []; | ||
} | ||
|
||
if ($node->name->toString() !== 'dispatch') { | ||
return []; | ||
} | ||
|
||
// all good | ||
if (count($node->getArgs()) === 1) { | ||
return []; | ||
} | ||
|
||
$callerType = $scope->getType($node->var); | ||
if (! $callerType instanceof ObjectType) { | ||
return []; | ||
} | ||
|
||
if (! $callerType->isInstanceOf(ClassName::EVENT_DISPATCHER_INTERFACE)->yes()) { | ||
return []; | ||
} | ||
|
||
$ruleError = RuleErrorBuilder::message(self::ERROR_MESSAGE)->build(); | ||
return [$ruleError]; | ||
} | ||
} |
13 changes: 13 additions & 0 deletions
13
stubs/Symfony/Component/EventDispatcher/EventDispatcherInterface.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Symfony\Component\EventDispatcher; | ||
|
||
if (interface_exists(EventDispatcherInterface::class)) { | ||
return; | ||
} | ||
|
||
interface EventDispatcherInterface | ||
{ | ||
} |
Oops, something went wrong.