From 4d06311e0d9b91b6085ff757c1b5b78857d36b5e Mon Sep 17 00:00:00 2001 From: Alies Lapatsin <5278175+alies-dev@users.noreply.github.com> Date: Tue, 23 Jun 2026 12:46:20 +0200 Subject: [PATCH] Prevent ForbidMethodDeclaration sniff aborting on deprecated traits is_subclass_of()/method_exists() autoload the analysed class. On PHP 8.5 the #[\Deprecated] attribute is honoured on traits, so loading a class that uses a deprecated trait emits a deprecation ("Trait X used by Y is deprecated"). PHP_CodeSniffer turns that into an exception, aborting the whole file check with an Internal.Exception. Silence deprecations around the reflection calls (they are a side effect of the analysed code, not of the sniff) and add a regression test that fails on the leaked deprecation. --- .../Classes/ForbidMethodDeclarationSniff.php | 21 +++++-- .../ForbidMethodDeclarationSniffTest.php | 59 +++++++++++++++++++ .../ChildDeclaringForbiddenMethod.php | 13 ++++ .../ChildWithoutForbiddenMethod.php | 11 ++++ .../DeprecatedTrait.php | 8 +++ .../ForbiddenParent.php | 7 +++ 6 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 tests/Sniffs/Classes/ForbidMethodDeclarationSniffTest.php create mode 100644 tests/Sniffs/Classes/data/ForbidMethodDeclarationSniff/ChildDeclaringForbiddenMethod.php create mode 100644 tests/Sniffs/Classes/data/ForbidMethodDeclarationSniff/ChildWithoutForbiddenMethod.php create mode 100644 tests/Sniffs/Classes/data/ForbidMethodDeclarationSniff/DeprecatedTrait.php create mode 100644 tests/Sniffs/Classes/data/ForbidMethodDeclarationSniff/ForbiddenParent.php diff --git a/IxDFCodingStandard/Sniffs/Classes/ForbidMethodDeclarationSniff.php b/IxDFCodingStandard/Sniffs/Classes/ForbidMethodDeclarationSniff.php index f4de1a9..4038a27 100644 --- a/IxDFCodingStandard/Sniffs/Classes/ForbidMethodDeclarationSniff.php +++ b/IxDFCodingStandard/Sniffs/Classes/ForbidMethodDeclarationSniff.php @@ -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; } @@ -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(); + } + } } diff --git a/tests/Sniffs/Classes/ForbidMethodDeclarationSniffTest.php b/tests/Sniffs/Classes/ForbidMethodDeclarationSniffTest.php new file mode 100644 index 0000000..4151e4b --- /dev/null +++ b/tests/Sniffs/Classes/ForbidMethodDeclarationSniffTest.php @@ -0,0 +1,59 @@ + '\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); + } +} diff --git a/tests/Sniffs/Classes/data/ForbidMethodDeclarationSniff/ChildDeclaringForbiddenMethod.php b/tests/Sniffs/Classes/data/ForbidMethodDeclarationSniff/ChildDeclaringForbiddenMethod.php new file mode 100644 index 0000000..c3b44d6 --- /dev/null +++ b/tests/Sniffs/Classes/data/ForbidMethodDeclarationSniff/ChildDeclaringForbiddenMethod.php @@ -0,0 +1,13 @@ +