Skip to content

Commit

Permalink
Merge pull request #9 from TomasVotruba/tv-downgrade
Browse files Browse the repository at this point in the history
Add couple handy phpstan rules
  • Loading branch information
TomasVotruba authored Dec 3, 2024
2 parents b532455 + 912fc40 commit 7da93e1
Show file tree
Hide file tree
Showing 13 changed files with 545 additions and 0 deletions.
40 changes: 40 additions & 0 deletions src/Command/DowngradeCommand.php
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;
}
}
18 changes: 18 additions & 0 deletions src/Enum/ClassName.php
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';
}
101 changes: 101 additions & 0 deletions src/PHPStan/Rule/NoConstructorOverrideRule.php
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];
}
}
59 changes: 59 additions & 0 deletions src/PHPStan/Rule/NoDocumentMockingRule.php
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 [];
}
}
61 changes: 61 additions & 0 deletions src/PHPStan/Rule/NoListenerWithoutContractRule.php
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];
}
}
104 changes: 104 additions & 0 deletions src/PHPStan/Rule/NoStringInGetSubscribedEventsRule.php
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 [];
}
}
Loading

0 comments on commit 7da93e1

Please sign in to comment.