-
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.
Merge pull request #9 from TomasVotruba/tv-downgrade
Add couple handy phpstan rules
- Loading branch information
Showing
13 changed files
with
545 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,40 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace TomasVotruba\Handyman\Command; | ||
|
||
use Nette\Utils\Json; | ||
use Symfony\Component\Console\Command\Command; | ||
use Symfony\Component\Console\Input\InputInterface; | ||
use Symfony\Component\Console\Output\OutputInterface; | ||
use Symfony\Component\Console\Style\SymfonyStyle; | ||
|
||
final class DowngradeCommand extends Command | ||
{ | ||
public function __construct( | ||
private readonly SymfonyStyle $symfonyStyle, | ||
) { | ||
parent::__construct(); | ||
} | ||
|
||
protected function configure(): void | ||
{ | ||
$this->setName('downgrade'); | ||
$this->setDescription( | ||
'Prepare setup for downgrading package with Rector on release - rector.php config, Scoper and Github Workflow' | ||
); | ||
} | ||
|
||
protected function execute(InputInterface $input, OutputInterface $output): int | ||
{ | ||
// @todo | ||
// @todo copy prepared workflow | ||
// @todo create /build with config | ||
// @todo update composer.json with no PHP version check | ||
|
||
$this->symfonyStyle->success('Done'); | ||
|
||
return self::SUCCESS; | ||
} | ||
} |
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,18 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace TomasVotruba\Handyman\Enum; | ||
|
||
final class ClassName | ||
{ | ||
/** | ||
* @var string | ||
*/ | ||
public const EVENT_DISPATCHER_INTERFACE = 'Symfony\Component\EventDispatcher\EventDispatcherInterface'; | ||
|
||
/** | ||
* @var string | ||
*/ | ||
public const FORM_EVENTS = 'Symfony\Component\Form\FormEvents'; | ||
} |
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,101 @@ | ||
<?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 []; | ||
} | ||
|
||
if ($node->stmts === null) { | ||
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,59 @@ | ||
<?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; | ||
|
||
/** | ||
* @implements Rule<MethodCall> | ||
*/ | ||
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,104 @@ | ||
<?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\Reflection\ClassReflection; | ||
use PHPStan\Rules\Rule; | ||
use PHPStan\Rules\RuleError; | ||
use PHPStan\Rules\RuleErrorBuilder; | ||
use TomasVotruba\Handyman\Enum\ClassName; | ||
|
||
/** | ||
* @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->stmts === null) { | ||
return []; | ||
} | ||
|
||
if ($node->name->toString() !== 'getSubscribedEvents') { | ||
return []; | ||
} | ||
|
||
$classReflection = $scope->getClassReflection(); | ||
if (! $classReflection instanceof ClassReflection) { | ||
return []; | ||
} | ||
|
||
// 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; | ||
|
||
if ($classConstFetch->class instanceof Expr) { | ||
continue; | ||
} | ||
|
||
// skip Symfony FormEvents::class | ||
if ($classConstFetch->class->toString() === ClassName::FORM_EVENTS) { | ||
continue; | ||
} | ||
|
||
if ($classConstFetch->name instanceof Expr) { | ||
continue; | ||
} | ||
|
||
if ($classConstFetch->name->toString() === 'class') { | ||
continue; | ||
} | ||
|
||
continue; | ||
} | ||
|
||
$ruleError = RuleErrorBuilder::message(self::ERROR_MESSAGE)->build(); | ||
return [$ruleError]; | ||
} | ||
|
||
return []; | ||
} | ||
} |
Oops, something went wrong.