Skip to content

Commit

Permalink
move couple handy rules here
Browse files Browse the repository at this point in the history
  • Loading branch information
TomasVotruba committed Dec 3, 2024
1 parent 55ec769 commit 65dbf5e
Show file tree
Hide file tree
Showing 12 changed files with 478 additions and 0 deletions.
13 changes: 13 additions & 0 deletions src/Enum/ClassName.php
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';
}
97 changes: 97 additions & 0 deletions src/PHPStan/Rule/NoConstructorOverrideRule.php
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 {

Check failure on line 72 in src/PHPStan/Rule/NoConstructorOverrideRule.php

View workflow job for this annotation

GitHub Actions / PHPStan

Parameter #1 $nodes of method PhpParser\NodeFinder::findFirst() expects array<PhpParser\Node>|PhpParser\Node, array<PhpParser\Node\Stmt>|null given.
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];
}
}
56 changes: 56 additions & 0 deletions src/PHPStan/Rule/NoDocumentMockingRule.php
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

Check failure on line 13 in src/PHPStan/Rule/NoDocumentMockingRule.php

View workflow job for this annotation

GitHub Actions / PHPStan

Class TomasVotruba\Handyman\PHPStan\Rule\NoDocumentMockingRule implements generic interface PHPStan\Rules\Rule but does not specify its types: TNodeType
{
/**
* @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];
}
}
88 changes: 88 additions & 0 deletions src/PHPStan/Rule/NoStringInGetSubscribedEventsRule.php
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)) {

Check failure on line 52 in src/PHPStan/Rule/NoStringInGetSubscribedEventsRule.php

View workflow job for this annotation

GitHub Actions / PHPStan

Cannot call method implementsInterface() on PHPStan\Reflection\ClassReflection|null.
return [];
}

$nodeFinder = new NodeFinder();

/** @var ArrayItem[] $arrayItems */
$arrayItems = $nodeFinder->findInstanceOf($node->stmts, ArrayItem::class);

Check failure on line 59 in src/PHPStan/Rule/NoStringInGetSubscribedEventsRule.php

View workflow job for this annotation

GitHub Actions / PHPStan

Parameter #1 $nodes of method PhpParser\NodeFinder::findInstanceOf() expects array<PhpParser\Node>|PhpParser\Node, array<PhpParser\Node\Stmt>|null given.

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

View workflow job for this annotation

GitHub Actions / PHPStan

Call to an undefined method PhpParser\Node\Expr|PhpParser\Node\Name::toString().

Check failure on line 71 in src/PHPStan/Rule/NoStringInGetSubscribedEventsRule.php

View workflow job for this annotation

GitHub Actions / PHPStan

Class Symfony\Component\Form\FormEvents not found.
continue;
}

if ($classConstFetch->name->toString() === 'class') {

Check failure on line 75 in src/PHPStan/Rule/NoStringInGetSubscribedEventsRule.php

View workflow job for this annotation

GitHub Actions / PHPStan

Call to an undefined method PhpParser\Node\Expr|PhpParser\Node\Identifier::toString().
continue;
}

continue;
}

$ruleError = RuleErrorBuilder::message(self::ERROR_MESSAGE)->build();
return [$ruleError];
}

return [];
}
}
60 changes: 60 additions & 0 deletions src/PHPStan/Rule/SingleArgEventDispatchRule.php
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

Check failure on line 17 in src/PHPStan/Rule/SingleArgEventDispatchRule.php

View workflow job for this annotation

GitHub Actions / PHPStan

Class TomasVotruba\Handyman\PHPStan\Rule\SingleArgEventDispatchRule implements generic interface PHPStan\Rules\Rule but does not specify its types: TNodeType
{
/**
* @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];
}
}
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
{
}
Loading

0 comments on commit 65dbf5e

Please sign in to comment.