Skip to content
Merged
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
21 changes: 17 additions & 4 deletions IxDFCodingStandard/Sniffs/Classes/ForbidMethodDeclarationSniff.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,8 @@ public function process(File $phpcsFile, $classPointer): void
$fqcn = ClassHelper::getFullyQualifiedName($phpcsFile, $classPointer);
foreach ($this->forbiddenMethods as $typeAndMethod => $replacement) {
[$type, $method] = explode('::', $typeAndMethod);
if (! is_subclass_of($fqcn, $type)) {
continue;
}

if (! method_exists($fqcn, $method)) {
if (! $this->declaresForbiddenMethod($fqcn, $type, $method)) {
continue;
}

Expand All @@ -46,4 +43,20 @@ public function process(File $phpcsFile, $classPointer): void
);
}
}

/** @param class-string $fqcn */
private function declaresForbiddenMethod(string $fqcn, string $type, string $method): bool
{
// is_subclass_of()/method_exists() autoload $fqcn. The analysed class may use a deprecated trait
// or extend a deprecated class, which emits a deprecation on PHP 8.5+ (e.g. "Trait X used by Y is
// deprecated"). That deprecation is a side effect of the analysed code, not of this sniff, and would
// otherwise be turned into an exception that aborts the whole file check, so it is silenced here.
set_error_handler(static fn(): bool => true, \E_DEPRECATED | \E_USER_DEPRECATED);

try {
return is_subclass_of($fqcn, $type) && method_exists($fqcn, $method);
} finally {
restore_error_handler();
}
}
}
59 changes: 59 additions & 0 deletions tests/Sniffs/Classes/ForbidMethodDeclarationSniffTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php declare(strict_types=1);

namespace IxDFCodingStandard\Sniffs\Classes;

use IxDFCodingStandard\Sniffs\Classes\data\ForbidMethodDeclarationSniff\ForbiddenParent;
use IxDFCodingStandard\TestCase;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\RequiresPhp;
use PHPUnit\Framework\Attributes\Test;

#[CoversClass(ForbidMethodDeclarationSniff::class)]
final class ForbidMethodDeclarationSniffTest extends TestCase
{
private const FORBIDDEN_METHODS = [
ForbiddenParent::class.'::shouldSend' => '\App\Notifications\Support\ShouldCheckConditionsBeforeSendingOut::shouldBeSent',
];

#[Test]
#[RequiresPhp('>= 8.5.0')]
public function it_reports_forbidden_method_even_when_the_class_uses_a_deprecated_trait(): void
{
// #[\Deprecated] is only allowed on traits since PHP 8.5 (earlier versions reject the fixture at compile time).
// Resolving the class autoloads it; using a deprecated trait then emits a deprecation, and the sniff must
// silence it instead of letting it abort the whole file check (Internal.Exception).
$deprecations = [];
set_error_handler(
static function (int $errno, string $message) use (&$deprecations): bool {
$deprecations[] = $message;

return true;
},
\E_DEPRECATED | \E_USER_DEPRECATED
);

try {
$report = self::checkFile(
__DIR__.'/data/ForbidMethodDeclarationSniff/ChildDeclaringForbiddenMethod.php',
['forbiddenMethods' => self::FORBIDDEN_METHODS]
);
} finally {
restore_error_handler();
}

self::assertSame([], $deprecations, 'The sniff must not leak deprecations triggered while autoloading the analysed class.');
self::assertSame(1, $report->getErrorCount());
self::assertSniffError($report, 5, ForbidMethodDeclarationSniff::FORBIDDEN_METHOD_DECLARATION);
}

#[Test]
public function it_does_not_report_when_the_subclass_does_not_declare_the_forbidden_method(): void
{
$report = self::checkFile(
__DIR__.'/data/ForbidMethodDeclarationSniff/ChildWithoutForbiddenMethod.php',
['forbiddenMethods' => self::FORBIDDEN_METHODS]
);

self::assertNoSniffErrorInFile($report);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php declare(strict_types=1);

namespace IxDFCodingStandard\Sniffs\Classes\data\ForbidMethodDeclarationSniff;

final class ChildDeclaringForbiddenMethod extends ForbiddenParent
{
use DeprecatedTrait;

public function shouldSend(): bool
{
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php declare(strict_types=1);

namespace IxDFCodingStandard\Sniffs\Classes\data\ForbidMethodDeclarationSniff;

final class ChildWithoutForbiddenMethod extends ForbiddenParent
{
public function send(): bool
{
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php declare(strict_types=1);

namespace IxDFCodingStandard\Sniffs\Classes\data\ForbidMethodDeclarationSniff;

#[\Deprecated(message: 'Use composition instead.')]
trait DeprecatedTrait
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php declare(strict_types=1);

namespace IxDFCodingStandard\Sniffs\Classes\data\ForbidMethodDeclarationSniff;

class ForbiddenParent
{
}