From c49ce4f0a9a69f910b1a990aae4ed4f28ef92244 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Thu, 6 Aug 2026 14:31:38 +0700 Subject: [PATCH 1/2] feat: added unconventional exception name detection --- src/Sniff/ExceptionNameSniff.php | 15 +++- tests/Unit/Fix/ExceptionNameFixerTest.php | 18 +++++ tests/Unit/Sniff/ExceptionNameSniffTest.php | 79 +++++++++++++++++++++ 3 files changed, 111 insertions(+), 1 deletion(-) diff --git a/src/Sniff/ExceptionNameSniff.php b/src/Sniff/ExceptionNameSniff.php index cb13c68..635480f 100644 --- a/src/Sniff/ExceptionNameSniff.php +++ b/src/Sniff/ExceptionNameSniff.php @@ -30,6 +30,13 @@ final class ExceptionNameSniff extends AbstractSniff implements Fixable 'Throwable', ]; + /** @var list */ + private const array UNCONVENTIONAL_EXCEPTION_NAMES = [ + 'com_exception', + 'mysqli_sql_exception', + 'SoapFault', + ]; + private const string CLASSNAME_PATTERN = '/]*>([^<]*)<\/classname>/'; public static function getCode(): string @@ -104,7 +111,13 @@ public function process(\DOMDocument $document, File $file): array public static function looksLikeException(string $text): bool { - $parts = explode('\\', $text); + $className = ltrim($text, '\\'); + + if (in_array($className, self::UNCONVENTIONAL_EXCEPTION_NAMES, true)) { + return true; + } + + $parts = explode('\\', $className); $baseName = end($parts); return array_any(self::DEFAULT_SUFFIXES, static fn(string $suffix): bool => str_ends_with($baseName, $suffix)); diff --git a/tests/Unit/Fix/ExceptionNameFixerTest.php b/tests/Unit/Fix/ExceptionNameFixerTest.php index 832fd32..512315b 100644 --- a/tests/Unit/Fix/ExceptionNameFixerTest.php +++ b/tests/Unit/Fix/ExceptionNameFixerTest.php @@ -65,6 +65,24 @@ public function itReplacesSimpleClassnameTags(): void self::assertSame(1, $result->applied); } + #[Test] + public function itReplacesUnconventionalExceptionNameElements(): void + { + $content = 'SoapFault'; + $document = $this->createDocument($content); + $source = new File('file.xml', $content); + + $violations = new ExceptionNameSniff()->process($document, $source); + + self::assertCount(1, $violations); + + $fix = new ExceptionNameFixer()->process($violations[0]); + $result = new FixApplier()->apply($source, [$fix]); + + self::assertSame('SoapFault', $result->file->content); + self::assertSame(1, $result->applied); + } + #[Test] public function itPreservesClassnameAttributes(): void { diff --git a/tests/Unit/Sniff/ExceptionNameSniffTest.php b/tests/Unit/Sniff/ExceptionNameSniffTest.php index 2a7f349..d7fc700 100644 --- a/tests/Unit/Sniff/ExceptionNameSniffTest.php +++ b/tests/Unit/Sniff/ExceptionNameSniffTest.php @@ -11,6 +11,7 @@ use DocbookCS\Violation\SourceRange; use DocbookCS\Violation\Violation; use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; @@ -64,6 +65,12 @@ public function itDoesNotFlagRegularClassnames(): void self::assertSame([], $violations); } + #[Test, DataProvider('knownExceptionNameProvider')] + public function itRecognisesAllExceptionNames(string $name): void + { + self::assertTrue(ExceptionNameSniff::looksLikeException($name)); + } + #[Test] public function itFlagsExceptionSuffix(): void { @@ -235,4 +242,76 @@ public function itRejectsSourceThatDoesNotMatchTheParsedDocument(): void new File('file.xml', 'OtherException'), ); } + + /** @return iterable */ + public static function knownExceptionNameProvider(): iterable + { + $names = [ + 'ArgumentCountError', + 'ArithmeticError', + 'AssertionError', + 'BadFunctionCallException', + 'BadMethodCallException', + 'ClosedGeneratorException', + 'CompileError', + 'com_exception', + 'DateError', + 'DateException', + 'DateInvalidOperationException', + 'DateInvalidTimeZoneException', + 'DateMalformedIntervalStringException', + 'DateMalformedPeriodStringException', + 'DateMalformedStringException', + 'DateObjectError', + 'DateRangeError', + 'DivisionByZeroError', + 'DomainException', + 'DOMException', + 'Dom\\DOMException', + 'Error', + 'ErrorException', + 'Exception', + 'FFI\\Exception', + 'FFI\\ParserException', + 'FiberError', + 'Filter\\FilterException', + 'Filter\\FilterFailedException', + 'IntlException', + 'InvalidArgumentException', + 'JsonException', + 'LengthException', + 'LogicException', + 'mysqli_sql_exception', + 'OutOfBoundsException', + 'OutOfRangeException', + 'OverflowException', + 'ParseError', + 'PDOException', + 'PharException', + 'Random\\BrokenRandomEngineError', + 'Random\\RandomError', + 'Random\\RandomException', + 'RangeException', + 'ReflectionException', + 'RequestParseBodyException', + 'RuntimeException', + 'SNMPException', + 'SoapFault', + 'SodiumException', + 'SQLite3Exception', + 'TypeError', + 'UnderflowException', + 'UnexpectedValueException', + 'UnhandledMatchError', + 'Uri\\InvalidUriException', + 'Uri\\UriError', + 'Uri\\UriException', + 'Uri\\WhatWg\\InvalidUrlException', + 'ValueError', + ]; + + foreach ($names as $name) { + yield $name => [$name]; + } + } } From 33f492f26ee2ef0a625f3e624587006d59b50ee4 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Thu, 6 Aug 2026 15:18:02 +0700 Subject: [PATCH 2/2] feat: extended exception name detection with extensions --- src/Sniff/ExceptionNameSniff.php | 40 ++++++++++++++++++++- tests/Unit/Sniff/ExceptionNameSniffTest.php | 30 +++++++++++++++- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/src/Sniff/ExceptionNameSniff.php b/src/Sniff/ExceptionNameSniff.php index 635480f..a7668e7 100644 --- a/src/Sniff/ExceptionNameSniff.php +++ b/src/Sniff/ExceptionNameSniff.php @@ -31,12 +31,50 @@ final class ExceptionNameSniff extends AbstractSniff implements Fixable ]; /** @var list */ - private const array UNCONVENTIONAL_EXCEPTION_NAMES = [ + private const array UNCONVENTIONAL_BUILT_IN_EXCEPTION_NAMES = [ 'com_exception', 'mysqli_sql_exception', 'SoapFault', ]; + /** @var list */ + private const array UNCONVENTIONAL_EXTENSION_EXCEPTION_NAMES = [ + 'parallel\\Channel\\Error\\Closed', + 'parallel\\Channel\\Error\\Existence', + 'parallel\\Channel\\Error\\IllegalValue', + 'parallel\\Events\\Error\\Existence', + 'parallel\\Events\\Error\\Timeout', + 'parallel\\Events\\Input\\Error\\Existence', + 'parallel\\Events\\Input\\Error\\IllegalValue', + 'parallel\\Future\\Error\\Cancelled', + 'parallel\\Future\\Error\\Foreign', + 'parallel\\Future\\Error\\Killed', + 'parallel\\Runtime\\Error\\Bootstrap', + 'parallel\\Runtime\\Error\\Closed', + 'parallel\\Runtime\\Error\\IllegalFunction', + 'parallel\\Runtime\\Error\\IllegalInstruction', + 'parallel\\Runtime\\Error\\IllegalParameter', + 'parallel\\Runtime\\Error\\IllegalReturn', + 'parallel\\Runtime\\Error\\IllegalVariable', + 'parallel\\Runtime\\Error\\Killed', + 'parallel\\Sync\\Error\\IllegalValue', + 'svmexception', + 'Swoole\\Exception\\ArrayKeyNotExists', + 'Yaf\\Exception\\DispatchFailed', + 'Yaf\\Exception\\LoadFailed', + 'Yaf\\Exception\\LoadFailed\\Action', + 'Yaf\\Exception\\LoadFailed\\Controller', + 'Yaf\\Exception\\LoadFailed\\Module', + 'Yaf\\Exception\\LoadFailed\\View', + 'Yaf\\Exception\\RouterFailed', + ]; + + /** @var list */ + private const array UNCONVENTIONAL_EXCEPTION_NAMES = [ + ...self::UNCONVENTIONAL_BUILT_IN_EXCEPTION_NAMES, + ...self::UNCONVENTIONAL_EXTENSION_EXCEPTION_NAMES, + ]; + private const string CLASSNAME_PATTERN = '/]*>([^<]*)<\/classname>/'; public static function getCode(): string diff --git a/tests/Unit/Sniff/ExceptionNameSniffTest.php b/tests/Unit/Sniff/ExceptionNameSniffTest.php index d7fc700..42827f9 100644 --- a/tests/Unit/Sniff/ExceptionNameSniffTest.php +++ b/tests/Unit/Sniff/ExceptionNameSniffTest.php @@ -66,7 +66,7 @@ public function itDoesNotFlagRegularClassnames(): void } #[Test, DataProvider('knownExceptionNameProvider')] - public function itRecognisesAllExceptionNames(string $name): void + public function itRecognisesAllKnownExceptionNames(string $name): void { self::assertTrue(ExceptionNameSniff::looksLikeException($name)); } @@ -288,6 +288,25 @@ public static function knownExceptionNameProvider(): iterable 'ParseError', 'PDOException', 'PharException', + 'parallel\\Channel\\Error\\Closed', + 'parallel\\Channel\\Error\\Existence', + 'parallel\\Channel\\Error\\IllegalValue', + 'parallel\\Events\\Error\\Existence', + 'parallel\\Events\\Error\\Timeout', + 'parallel\\Events\\Input\\Error\\Existence', + 'parallel\\Events\\Input\\Error\\IllegalValue', + 'parallel\\Future\\Error\\Cancelled', + 'parallel\\Future\\Error\\Foreign', + 'parallel\\Future\\Error\\Killed', + 'parallel\\Runtime\\Error\\Bootstrap', + 'parallel\\Runtime\\Error\\Closed', + 'parallel\\Runtime\\Error\\IllegalFunction', + 'parallel\\Runtime\\Error\\IllegalInstruction', + 'parallel\\Runtime\\Error\\IllegalParameter', + 'parallel\\Runtime\\Error\\IllegalReturn', + 'parallel\\Runtime\\Error\\IllegalVariable', + 'parallel\\Runtime\\Error\\Killed', + 'parallel\\Sync\\Error\\IllegalValue', 'Random\\BrokenRandomEngineError', 'Random\\RandomError', 'Random\\RandomException', @@ -299,6 +318,8 @@ public static function knownExceptionNameProvider(): iterable 'SoapFault', 'SodiumException', 'SQLite3Exception', + 'svmexception', + 'Swoole\\Exception\\ArrayKeyNotExists', 'TypeError', 'UnderflowException', 'UnexpectedValueException', @@ -308,6 +329,13 @@ public static function knownExceptionNameProvider(): iterable 'Uri\\UriException', 'Uri\\WhatWg\\InvalidUrlException', 'ValueError', + 'Yaf\\Exception\\DispatchFailed', + 'Yaf\\Exception\\LoadFailed', + 'Yaf\\Exception\\LoadFailed\\Action', + 'Yaf\\Exception\\LoadFailed\\Controller', + 'Yaf\\Exception\\LoadFailed\\Module', + 'Yaf\\Exception\\LoadFailed\\View', + 'Yaf\\Exception\\RouterFailed', ]; foreach ($names as $name) {