Skip to content

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

Closed
wants to merge 51 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
f168e3d
Create sniff class base for exception interface naming
SenseException Jun 3, 2018
b6775ae
Install phpunit
SenseException Jun 7, 2018
c792277
Create tests for exception interface naming sniff
SenseException Jun 7, 2018
a3bfa8b
Add phpunit to the travis build
SenseException Jun 7, 2018
06def93
Make FooException of input-classes dirty
SenseException Jun 8, 2018
e3df582
Adapt expected report to new phpcs result
SenseException Jun 8, 2018
051b168
Remove instruction comment
SenseException Jun 9, 2018
6634b83
Use UseStatementHelper to parse imported fqcn
SenseException Jun 11, 2018
3eda2a0
Handle alias for imported interfaces
SenseException Jun 12, 2018
8b384f4
Add test for comma separated namespaces
SenseException Jun 12, 2018
c8f3bf2
Add test for group use namespaces
SenseException Jun 12, 2018
4da75bb
Provide exception interface for phpcbf comparison test
SenseException Jun 12, 2018
dac1803
Refactor to a cleaner coding style
SenseException Jun 13, 2018
6d7d1da
Improve parsing of interface names
SenseException Jun 14, 2018
c1c667f
Optimize phpunit config and create a custom bootstrap
SenseException Jun 14, 2018
7eebe88
Install phpstan for usage in travis build
SenseException Jun 14, 2018
d876092
Refactor tests to use SlevomatCodingStandard assertions
SenseException Jun 17, 2018
482423b
Move tests to different namespace for SlevomatCodingStandard sniff lo…
SenseException Jun 17, 2018
a80eeb5
Use early return on correct exception naming
SenseException Jun 17, 2018
75fbea8
Move test exception interfaces into subdirectory
SenseException Jun 25, 2018
fa28401
Add type cast for possible nullable return value
SenseException Jun 25, 2018
36f078e
Create tests for exception class sniff
SenseException Jun 28, 2018
d866b41
Create first exception class sniff draft
SenseException Jun 28, 2018
ddfbb48
Add tests for Throwable in same namespace
SenseException Jul 10, 2018
3d26ff0
Move parsing of use statements into helper class
SenseException Jul 10, 2018
480c8fd
Set codesniffer coding style for tests
SenseException Jul 17, 2018
66c9710
Set phpstan 0.10 as minimum requirement
SenseException Jul 17, 2018
b1fe013
Create phpstan configuration
SenseException Jul 17, 2018
1600f87
Rename namespace root to DoctrineCodingStandard
SenseException Jul 17, 2018
acf4853
Move logic of check for Throwable to UseStatementHelper
SenseException Jul 19, 2018
418fd0c
Move test classes into one file for valid interface naming sniff
SenseException Jul 22, 2018
a954c74
Move test classes into one file for valid class naming sniff
SenseException Jul 22, 2018
0257a9d
Check for non-abstract exceptions to be final
SenseException Jul 22, 2018
e55478f
Move test classes into one file for invalid class naming sniff
SenseException Jul 23, 2018
758711f
Move test classes into one file for invalid interface naming sniff
SenseException Jul 23, 2018
b8e47b4
Extend fixture classes for test cases
SenseException Jul 24, 2018
5755289
Renaming of variables and exception texts
SenseException Sep 6, 2018
bdff27e
Move logic of private methods into helper classes
SenseException Sep 6, 2018
628e6e0
Use early exit for class name checks
SenseException Sep 6, 2018
e1fca9b
Update return type in docs for register method
SenseException Sep 6, 2018
29d325d
Fix of check for valid class name
SenseException Sep 6, 2018
ecdf230
Use Slevomat helper to get abstract or final token
SenseException Sep 7, 2018
c5fff4e
Handle classes that aren't extending exceptions
SenseException Sep 7, 2018
ca67ff2
Rearrange of the variable rows in the class naming sniff
SenseException Sep 8, 2018
195b7a6
Replace explode on token-string with token parser logic
SenseException Sep 9, 2018
f2b4a57
Fix standard name & namespace
Majkl578 Sep 10, 2018
104bc2a
Improve exception naming sniffs
Majkl578 Sep 10, 2018
b526a5d
Exclude redundant SlevomatCodingStandard.Classes.SuperfluousException…
Majkl578 Sep 12, 2018
2365af8
Fix of typo of interface name
SenseException Sep 15, 2018
262246e
Fix of merged and rebased code changes
SenseException Oct 3, 2018
d7a3331
Use hardcoded expected sniffer error code in test
SenseException Oct 3, 2018
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
2 changes: 2 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ install: travis_retry composer update --prefer-dist
script:
- vendor/bin/phpcs
- vendor/bin/phpcs $(find tests/input/* | sort) --report=summary --report-file=phpcs.log; diff tests/expected_report.txt phpcs.log
- vendor/bin/phpstan analyse
- vendor/bin/phpunit

stages:
- Validate against schema
Expand Down
11 changes: 10 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
],
"autoload": {
"psr-4": {
"Doctrine\\Sniffs\\": "lib/Doctrine/Sniffs"
"Doctrine\\": "lib/Doctrine"
}
},
"require": {
Expand All @@ -38,5 +38,14 @@
"branch-alias": {
"dev-master": "6.0.x-dev"
}
},
"require-dev": {
"phpunit/phpunit": "^7.2",
"phpstan/phpstan": "^0.10"
},
"autoload-dev": {
"psr-4": {
"Doctrine\\": "tests/Doctrine"
}
}
}
15 changes: 15 additions & 0 deletions lib/Doctrine/Helpers/ClassHelper.php
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'];
}
}
48 changes: 48 additions & 0 deletions lib/Doctrine/Helpers/InheritanceHelper.php
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));
}
}
41 changes: 41 additions & 0 deletions lib/Doctrine/Helpers/NamingHelper.php
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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about ExceptionInterface?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interfaces have to end with Exception. I can add ExceptionInterface to a test for a fail case.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interfaces have to end with Exception

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

Copy link
Member Author

Choose a reason for hiding this comment

The 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?

Copy link
Member

Choose a reason for hiding this comment

The 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);
}
}
61 changes: 61 additions & 0 deletions lib/Doctrine/Helpers/UseStatementHelper.php
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
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a fan of Helper classes or a helper-namespace, but I kept naming and usage of static methods for consistency. If we should use a different approach for outsourcing logic, please let me know.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It actually fits it better to use non-static class accepting File $phpcsFile in its constructor.

{
/**
* @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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought we have a rule for enforcing return on newline

Copy link
Member Author

Choose a reason for hiding this comment

The 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.

}
}
98 changes: 98 additions & 0 deletions lib/Doctrine/Sniffs/Classes/ExceptionClassNamingSniff.php
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
);
}
}
73 changes: 73 additions & 0 deletions lib/Doctrine/Sniffs/Classes/ExceptionInterfaceNamingSniff.php
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
);
}
}
4 changes: 2 additions & 2 deletions lib/Doctrine/ruleset.xml
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,6 @@
<rule ref="SlevomatCodingStandard.Classes.UnusedPrivateElements"/>
<!-- Forbid suffix "Abstract" for abstract classes -->
<rule ref="SlevomatCodingStandard.Classes.SuperfluousAbstractClassNaming"/>
<!-- Forbid suffix "Exception" for exception classes -->
<rule ref="SlevomatCodingStandard.Classes.SuperfluousExceptionNaming"/>
<!-- Forbid suffix "Interface" for interfaces -->
<rule ref="SlevomatCodingStandard.Classes.SuperfluousInterfaceNaming"/>
<!-- Require specific order of phpDoc annotations with empty newline between specific groups -->
Expand Down Expand Up @@ -475,4 +473,6 @@
<!-- turned off by PSR2 -> turning back on -->
<severity>5</severity>
</rule>


</ruleset>
Loading