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 @@ +