-
Notifications
You must be signed in to change notification settings - Fork 51
Exception naming sniffs #63
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f168e3d
b6775ae
c792277
a3bfa8b
06def93
e3df582
051b168
6634b83
3eda2a0
8b384f4
c8f3bf2
4da75bb
dac1803
6d7d1da
c1c667f
7eebe88
d876092
482423b
a80eeb5
75fbea8
fa28401
36f078e
d866b41
ddfbb48
3d26ff0
480c8fd
66c9710
b1fe013
1600f87
acf4853
418fd0c
a954c74
0257a9d
e55478f
758711f
b8e47b4
5755289
bdff27e
628e6e0
e1fca9b
29d325d
ecdf230
c5fff4e
ca67ff2
195b7a6
f2b4a57
104bc2a
b526a5d
2365af8
262246e
d7a3331
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Doctrine\Helpers; | ||
|
||
use PHP_CodeSniffer\Files\File; | ||
|
||
class ClassHelper | ||
{ | ||
public static function isAbstract(File $phpcsFile, int $classPointer) : bool | ||
{ | ||
return $phpcsFile->getClassProperties($classPointer)['is_abstract']; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Doctrine\Helpers; | ||
|
||
use PHP_CodeSniffer\Files\File; | ||
use SlevomatCodingStandard\Helpers\TokenHelper; | ||
use const T_COMMA; | ||
use const T_EXTENDS; | ||
use const T_OPEN_CURLY_BRACKET; | ||
use function trim; | ||
|
||
class InheritanceHelper | ||
{ | ||
/** | ||
* @return string[] | ||
*/ | ||
public static function getExtendedInterfaces(File $phpcsFile, int $pointer) : array | ||
{ | ||
$limit = TokenHelper::findNext($phpcsFile, [T_OPEN_CURLY_BRACKET], $pointer) - 1; | ||
$start = TokenHelper::findNext($phpcsFile, [T_EXTENDS], $pointer + 1, $limit); | ||
|
||
if ($start === null) { | ||
return []; | ||
} | ||
|
||
$extendedInterfaces = []; | ||
do { | ||
$localEnd = TokenHelper::findNextLocal($phpcsFile, [T_COMMA], $start + 1, $limit) ?? $limit; | ||
$extendedInterfaces[] = trim(TokenHelper::getContent($phpcsFile, $start + 1, $localEnd - 1)); | ||
$start = $localEnd; | ||
} while ($start < $limit); | ||
|
||
return UseStatementHelper::getRealNames($phpcsFile, $pointer, $extendedInterfaces); | ||
} | ||
|
||
public static function classExtendsException(File $phpcsFile, int $pointer) : bool | ||
{ | ||
$extendedName = $phpcsFile->findExtendedClassName($pointer); | ||
|
||
if ($extendedName === false) { | ||
return false; | ||
} | ||
|
||
return NamingHelper::hasExceptionSuffix(UseStatementHelper::getRealName($phpcsFile, $extendedName)); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Doctrine\Helpers; | ||
|
||
use Throwable; | ||
use function in_array; | ||
use function preg_match; | ||
|
||
final class NamingHelper | ||
{ | ||
public static function hasExceptionSuffix(string $className) : bool | ||
{ | ||
return preg_match('~Exception$~', $className) === 1; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about ExceptionInterface? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interfaces have to end with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
That's true only for doctrine code. Userland (or doctrine codebase itself) can be extending 3rd party exceptions with different naming convention. Detecting Exception(Interface)? is small step but easy win in such cases There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. True. It started for Doctrine code, but if someone else wants to use it, this might become a nuisance. But the detection of what is an Exception and what not depends on a naming convention. Your example uses a classic "end with Interface" convention, but what about a 3rd party exception naming convention without any suffixes or prefixes? This would make detecting exceptions with this sniff very hard. What about making the naming convention configurable? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you want to make this configurable, feel free. For me, handling ExceptionInterface would be good enough step, as such convention is common. |
||
} | ||
|
||
/** | ||
* @param string[] $names | ||
*/ | ||
public static function containsException(array $names) : bool | ||
{ | ||
foreach ($names as $name) { | ||
if (! self::hasExceptionSuffix($name)) { | ||
continue; | ||
} | ||
|
||
return true; | ||
} | ||
|
||
return false; | ||
} | ||
|
||
/** | ||
* @param string[] $names | ||
*/ | ||
public static function containsThrowable(array $names) : bool | ||
{ | ||
return in_array(Throwable::class, $names, true); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Doctrine\Helpers; | ||
|
||
use PHP_CodeSniffer\Files\File; | ||
use SlevomatCodingStandard\Helpers\NamespaceHelper; | ||
use SlevomatCodingStandard\Helpers\UseStatementHelper as UseHelper; | ||
use function array_map; | ||
use function ltrim; | ||
use function sprintf; | ||
|
||
class UseStatementHelper | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not a fan of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It actually fits it better to use non-static class accepting |
||
{ | ||
/** | ||
* @return string[] | ||
*/ | ||
public static function getUseStatements(File $phpcsFile) : array | ||
{ | ||
$importedClasses = []; | ||
foreach (UseHelper::getUseStatements($phpcsFile, 0) as $useStatement) { | ||
$importedClasses[$useStatement->getNameAsReferencedInFile()] = $useStatement->getFullyQualifiedTypeName(); | ||
} | ||
|
||
return $importedClasses; | ||
} | ||
|
||
/** | ||
* @param string[] $referencedNames | ||
* | ||
* @return string[] | ||
*/ | ||
public static function getRealNames(File $phpcsFile, int $pointer, array $referencedNames) : array | ||
{ | ||
$uses = self::getUseStatements($phpcsFile); | ||
$currentNamespace = NamespaceHelper::findCurrentNamespaceName($phpcsFile, $pointer); | ||
|
||
return array_map(static function (string $extendedInterface) use ($uses, $currentNamespace) : string { | ||
if ($extendedInterface[0] === '\\') { | ||
return ltrim($extendedInterface, '\\'); | ||
} | ||
|
||
if (isset($uses[$extendedInterface])) { | ||
return ltrim($uses[$extendedInterface], '\\'); | ||
} | ||
|
||
if ($currentNamespace === null) { | ||
return $extendedInterface; | ||
} | ||
|
||
return sprintf('%s\\%s', $currentNamespace, $extendedInterface); | ||
}, $referencedNames); | ||
} | ||
|
||
public static function getRealName(File $phpcsFile, string $referencedName) : string | ||
{ | ||
$uses = self::getUseStatements($phpcsFile); | ||
return $uses[$referencedName] ?? $referencedName; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought we have a rule for enforcing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That rule had to be added after my last commit, because the last build is green. After review, I'll rebase and fix the rest of the coding styles. |
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Doctrine\Sniffs\Classes; | ||
|
||
use Doctrine\Helpers\ClassHelper; | ||
use Doctrine\Helpers\InheritanceHelper; | ||
use Doctrine\Helpers\NamingHelper; | ||
use PHP_CodeSniffer\Files\File; | ||
use PHP_CodeSniffer\Sniffs\Sniff; | ||
use const T_CLASS; | ||
use function assert; | ||
|
||
final class ExceptionClassNamingSniff implements Sniff | ||
{ | ||
public const CODE_NOT_EXTENDING_EXCEPTION = 'NotExtendingException'; | ||
public const CODE_MISSING_SUFFIX = 'MissingSuffix'; | ||
public const CODE_SUPERFLUOUS_SUFFIX = 'SuperfluousSuffix'; | ||
|
||
/** | ||
* @return int[]|string[] | ||
*/ | ||
public function register() : array | ||
{ | ||
return [T_CLASS]; | ||
} | ||
|
||
/** | ||
* @param int $pointer | ||
* | ||
* @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingParameterTypeHint | ||
*/ | ||
public function process(File $phpcsFile, $pointer) : void | ||
{ | ||
$declarationName = $phpcsFile->getDeclarationName($pointer); | ||
assert($declarationName !== null); | ||
|
||
$isAbstract = ClassHelper::isAbstract($phpcsFile, $pointer); | ||
$extendsException = InheritanceHelper::classExtendsException($phpcsFile, $pointer); | ||
$hasSuffix = NamingHelper::hasExceptionSuffix($declarationName); | ||
|
||
if (! $hasSuffix && ! $extendsException) { | ||
// class Foo | ||
// class Foo extends Bar | ||
// abstract class Foo | ||
// abstract class Foo extends Bar | ||
|
||
return; | ||
} | ||
|
||
if (! $hasSuffix && ! $isAbstract && $extendsException) { | ||
// class FooProblem extends BarException | ||
|
||
return; | ||
} | ||
|
||
if ($hasSuffix && $isAbstract && $extendsException) { | ||
// class FooException extends BarException | ||
|
||
return; | ||
} | ||
|
||
if ($hasSuffix && $isAbstract && ! $extendsException) { | ||
// abstract class FooException extends Bar | ||
|
||
$phpcsFile->addError( | ||
'Abstract exception must extend another exception.', | ||
$pointer, | ||
self::CODE_NOT_EXTENDING_EXCEPTION | ||
); | ||
|
||
return; | ||
} | ||
|
||
if (! $hasSuffix && $isAbstract && $extendsException) { | ||
// class Foo extends BarException | ||
|
||
$phpcsFile->addError( | ||
'Abstract exceptions must use the "Exception" suffix.', | ||
$pointer, | ||
self::CODE_MISSING_SUFFIX | ||
); | ||
|
||
return; | ||
} | ||
|
||
assert($hasSuffix && ! $isAbstract); | ||
|
||
// class FooException | ||
|
||
$phpcsFile->addError( | ||
'Leaf exception classes must not end with the "Exception" suffix.', | ||
$pointer, | ||
self::CODE_SUPERFLUOUS_SUFFIX | ||
); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Doctrine\Sniffs\Classes; | ||
|
||
use Doctrine\Helpers\InheritanceHelper; | ||
use Doctrine\Helpers\NamingHelper; | ||
use PHP_CodeSniffer\Files\File; | ||
use PHP_CodeSniffer\Sniffs\Sniff; | ||
use const T_INTERFACE; | ||
|
||
final class ExceptionInterfaceNamingSniff implements Sniff | ||
{ | ||
private const CODE_NOT_AN_EXCEPTION = 'NotAnException'; | ||
|
||
/** | ||
* @return int[]|string[] | ||
*/ | ||
public function register() : array | ||
{ | ||
return [T_INTERFACE]; | ||
} | ||
|
||
/** | ||
* @param int $pointer | ||
* | ||
* @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingParameterTypeHint | ||
*/ | ||
public function process(File $phpcsFile, $pointer) : void | ||
{ | ||
$declarationName = $phpcsFile->getDeclarationName($pointer); | ||
$extendedInterfaces = InheritanceHelper::getExtendedInterfaces($phpcsFile, $pointer); | ||
$extendsThrowable = NamingHelper::containsThrowable($extendedInterfaces); | ||
$hasSuffix = NamingHelper::hasExceptionSuffix((string) $declarationName); | ||
|
||
if ($hasSuffix && $extendsThrowable) { | ||
// interface FooException extends Throwable | ||
return; | ||
} | ||
|
||
// assumes that an interface with the "Exception" suffix is a valid exception interface | ||
$extendsException = NamingHelper::containsException($extendedInterfaces); | ||
|
||
if ($hasSuffix && $extendsException) { | ||
// interface FooException extends BarException | ||
return; | ||
} | ||
|
||
if ($hasSuffix && ! $extendsException && ! $extendsThrowable) { | ||
// interface FooException | ||
$phpcsFile->addError( | ||
'Exception interface must extend another exception interface or Throwable.', | ||
$pointer, | ||
self::CODE_NOT_AN_EXCEPTION | ||
); | ||
|
||
return; | ||
} | ||
|
||
if (! $extendsException && ! $extendsThrowable) { | ||
return; | ||
} | ||
|
||
// interface Foo extends Throwable | ||
// interface Foo extends BarException | ||
$phpcsFile->addError( | ||
'Exception interface must end with the "Exception" name suffix.', | ||
$pointer, | ||
self::CODE_NOT_AN_EXCEPTION | ||
); | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.