Skip to content
Open
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
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/vendor
/phpunit.xml
/.php_cs.cache
composer.lock
.php-cs-fixer.cache
.phpunit.result.cache
1 change: 1 addition & 0 deletions extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ parameters:
- stubs/Money/MoneyParser.stub
rules:
- Ibexa\PHPStan\Rules\NoConfigResolverParametersInConstructorRule
- Ibexa\PHPStan\Rules\RequireAbstractionInDependenciesRule
Empty file added phpstan-baseline.neon
Empty file.
8 changes: 8 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
includes:
- phpstan-baseline.neon

parameters:
level: 8
paths:
- rules
- tests
checkMissingCallableSignature: true
ignoreErrors:
# Test fixture properties are intentionally write-only for testing purposes
-
message: "#^Property .* is never read, only written\\.$#"
path: tests/rules/Fixtures/RequireAbstractionInDependenciesFixture.php
116 changes: 116 additions & 0 deletions rules/RequireAbstractionInDependenciesRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\PHPStan\Rules;

use PhpParser\Node;
use PhpParser\Node\Expr\Error;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleError;
use PHPStan\Rules\RuleErrorBuilder;

/**
* @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\ClassMethod>
*/
final class RequireAbstractionInDependenciesRule implements Rule
{
private ReflectionProvider $reflectionProvider;

public function __construct(
ReflectionProvider $reflectionProvider
) {
$this->reflectionProvider = $reflectionProvider;
}

public function getNodeType(): string
{
return Node\Stmt\ClassMethod::class;
}

public function processNode(Node $node, Scope $scope): array
{
if (!$node->params) {
return [];
}

$errors = [];

foreach ($node->params as $param) {
$error = $this->validateParameter($param);
if ($error !== null) {
$errors[] = $error;
}
}

return $errors;
}

private function validateParameter(Node\Param $param): ?RuleError
{
if (!$param->type instanceof Node\Name) {
return null;
}

if ($param->var instanceof Error) {
return null;
}

$typeName = $param->type->toString();

// Skip if the type doesn't exist in reflection
if (!$this->reflectionProvider->hasClass($typeName)) {
return null;
}

$classReflection = $this->reflectionProvider->getClass($typeName);

// Skip interfaces - they are always acceptable
if ($classReflection->isInterface()) {
return null;
}

// Skip abstract classes - they are acceptable
if ($classReflection->isAbstract()) {
return null;
}

$reflection = $classReflection->getNativeReflection();

// This is a concrete class - check if it has interfaces or extends an abstract class
$interfaces = class_implements($typeName);
$parentClass = $reflection->getParentClass();
$hasAbstractParent = $parentClass && $parentClass->isAbstract();

// If there are no interfaces and no abstract parent, it's acceptable (no violation)
if (empty($interfaces) && !$hasAbstractParent) {
return null;
}

// Build error with suggestions
$suggestions = [];

if (!empty($interfaces)) {
$suggestions[] = 'Available interfaces: ' . implode(', ', $interfaces);
}

if ($hasAbstractParent) {
$suggestions[] = 'Abstract parent: ' . $parentClass->getName();
}

return RuleErrorBuilder::message(
sprintf(
'Parameter $%s uses concrete class %s instead of an interface or abstract class. %s',
is_string($param->var->name) ? $param->var->name : $param->var->name->getType(),
$typeName,
implode('. ', $suggestions)
)
)->build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Tests\PHPStan\Rules\Fixtures\RequireAbstractionInDependencies;

abstract class AbstractClass
{
abstract public function doSomethingAbstract(): void;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Tests\PHPStan\Rules\Fixtures\RequireAbstractionInDependencies;

class ClassWithoutInterface
{
public function doNothing(): void
{
// Implementation
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Tests\PHPStan\Rules\Fixtures\RequireAbstractionInDependencies;

class ConcreteClass implements TestInterface
{
public function doSomething(): void
{
// Implementation
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Tests\PHPStan\Rules\Fixtures\RequireAbstractionInDependencies;

class ConcreteClassExtendingAbstract extends AbstractClass
{
public function doSomethingAbstract(): void
{
// Implementation
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Tests\PHPStan\Rules\Fixtures\RequireAbstractionInDependencies;

interface TestInterface
{
public function doSomething(): void;
}
66 changes: 66 additions & 0 deletions tests/rules/Fixtures/RequireAbstractionInDependenciesFixture.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Tests\PHPStan\Rules\Fixtures;

use Ibexa\Tests\PHPStan\Rules\Fixtures\RequireAbstractionInDependencies\AbstractClass;
use Ibexa\Tests\PHPStan\Rules\Fixtures\RequireAbstractionInDependencies\ClassWithoutInterface;
use Ibexa\Tests\PHPStan\Rules\Fixtures\RequireAbstractionInDependencies\ConcreteClass;
use Ibexa\Tests\PHPStan\Rules\Fixtures\RequireAbstractionInDependencies\ConcreteClassExtendingAbstract;
use Ibexa\Tests\PHPStan\Rules\Fixtures\RequireAbstractionInDependencies\TestInterface;

final class RequireAbstractionInDependenciesFixture
{
private ConcreteClass $concreteClass;

private TestInterface $testInterface;

private ClassWithoutInterface $classWithoutInterface;

private AbstractClass $abstractClass;

private ConcreteClassExtendingAbstract $concreteExtendingAbstract;

public function __construct(
ConcreteClass $concreteClass,
TestInterface $testInterface,
ClassWithoutInterface $classWithoutInterface,
AbstractClass $abstractClass,
ConcreteClassExtendingAbstract $concreteExtendingAbstract
) {
$this->concreteClass = $concreteClass;
$this->testInterface = $testInterface;
$this->classWithoutInterface = $classWithoutInterface;
$this->abstractClass = $abstractClass;
$this->concreteExtendingAbstract = $concreteExtendingAbstract;
}

public function methodWithConcreteClass(ConcreteClass $class): void
{
}

public function methodWithInterface(TestInterface $interface): void
{
}

public function methodWithAbstractClass(AbstractClass $abstract): void
{
}

public function methodWithConcreteExtendingAbstract(ConcreteClassExtendingAbstract $concreteExtendingAbstract): void
{
}

public function methodWithoutInterface(ClassWithoutInterface $class): void
{
}

public function methodWithBuiltInTypes(string $str, int $num): void
{
}
}
53 changes: 53 additions & 0 deletions tests/rules/RequireAbstractionInDependenciesRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Tests\PHPStan\Rules;

use Ibexa\PHPStan\Rules\RequireAbstractionInDependenciesRule;
use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;

/**
* @extends \PHPStan\Testing\RuleTestCase<\Ibexa\PHPStan\Rules\RequireAbstractionInDependenciesRule>
*/
final class RequireAbstractionInDependenciesRuleTest extends RuleTestCase
{
protected function getRule(): Rule
{
return new RequireAbstractionInDependenciesRule(
$this->createReflectionProvider()
);
}

public function testRule(): void
{
$this->analyse(
[
__DIR__ . '/Fixtures/RequireAbstractionInDependenciesFixture.php',
],
[
[
'Parameter $concreteClass uses concrete class Ibexa\Tests\PHPStan\Rules\Fixtures\RequireAbstractionInDependencies\ConcreteClass instead of an interface or abstract class. Available interfaces: Ibexa\Tests\PHPStan\Rules\Fixtures\RequireAbstractionInDependencies\TestInterface',
29,
],
[
'Parameter $concreteExtendingAbstract uses concrete class Ibexa\Tests\PHPStan\Rules\Fixtures\RequireAbstractionInDependencies\ConcreteClassExtendingAbstract instead of an interface or abstract class. Abstract parent: Ibexa\Tests\PHPStan\Rules\Fixtures\RequireAbstractionInDependencies\AbstractClass',
29,
],
[
'Parameter $class uses concrete class Ibexa\Tests\PHPStan\Rules\Fixtures\RequireAbstractionInDependencies\ConcreteClass instead of an interface or abstract class. Available interfaces: Ibexa\Tests\PHPStan\Rules\Fixtures\RequireAbstractionInDependencies\TestInterface',
43,
],
[
'Parameter $concreteExtendingAbstract uses concrete class Ibexa\Tests\PHPStan\Rules\Fixtures\RequireAbstractionInDependencies\ConcreteClassExtendingAbstract instead of an interface or abstract class. Abstract parent: Ibexa\Tests\PHPStan\Rules\Fixtures\RequireAbstractionInDependencies\AbstractClass',
55,
],
]
);
}
}
Loading