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
53 changes: 52 additions & 1 deletion src/Sniff/ExceptionNameSniff.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,51 @@ final class ExceptionNameSniff extends AbstractSniff implements Fixable
'Throwable',
];

/** @var list<string> */
private const array UNCONVENTIONAL_BUILT_IN_EXCEPTION_NAMES = [
'com_exception',
'mysqli_sql_exception',
'SoapFault',
];

/** @var list<string> */
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<string> */
private const array UNCONVENTIONAL_EXCEPTION_NAMES = [
...self::UNCONVENTIONAL_BUILT_IN_EXCEPTION_NAMES,
...self::UNCONVENTIONAL_EXTENSION_EXCEPTION_NAMES,
];

private const string CLASSNAME_PATTERN = '/<classname\b[^>]*>([^<]*)<\/classname>/';

public static function getCode(): string
Expand Down Expand Up @@ -104,7 +149,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));
Expand Down
18 changes: 18 additions & 0 deletions tests/Unit/Fix/ExceptionNameFixerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,24 @@ public function itReplacesSimpleClassnameTags(): void
self::assertSame(1, $result->applied);
}

#[Test]
public function itReplacesUnconventionalExceptionNameElements(): void
{
$content = '<root><classname>SoapFault</classname></root>';
$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('<root><exceptionname>SoapFault</exceptionname></root>', $result->file->content);
self::assertSame(1, $result->applied);
}

#[Test]
public function itPreservesClassnameAttributes(): void
{
Expand Down
107 changes: 107 additions & 0 deletions tests/Unit/Sniff/ExceptionNameSniffTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -64,6 +65,12 @@ public function itDoesNotFlagRegularClassnames(): void
self::assertSame([], $violations);
}

#[Test, DataProvider('knownExceptionNameProvider')]
public function itRecognisesAllKnownExceptionNames(string $name): void
{
self::assertTrue(ExceptionNameSniff::looksLikeException($name));
}

#[Test]
public function itFlagsExceptionSuffix(): void
{
Expand Down Expand Up @@ -235,4 +242,104 @@ public function itRejectsSourceThatDoesNotMatchTheParsedDocument(): void
new File('file.xml', '<root><classname>OtherException</classname></root>'),
);
}

/** @return iterable<string, array{string}> */
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',
'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',
'RangeException',
'ReflectionException',
'RequestParseBodyException',
'RuntimeException',
'SNMPException',
'SoapFault',
'SodiumException',
'SQLite3Exception',
'svmexception',
'Swoole\\Exception\\ArrayKeyNotExists',
'TypeError',
'UnderflowException',
'UnexpectedValueException',
'UnhandledMatchError',
'Uri\\InvalidUriException',
'Uri\\UriError',
'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) {
yield $name => [$name];
}
}
}