-
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
Conversation
phpunit.xml.dist
Outdated
3) To run the tests against the database type the following from within the | ||
tests/ folder: phpunit -c <filename> ... | ||
Example: phpunit -c mysqlconf.xml AllTests | ||
--> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably needs to be removed since this repo does not have to do with dbms'es
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True. I guess we don't need the hint to copy the dist-file into a xml either, so I removed it.
That depends on whether those classes are part of the public API: if they are, go for it. @kukulich could you clarify whether those helpers are public API covered under Semver rules?
The assumption would make the sniff considerably easier, but strictly speaking, this isn't the case. I believe that we should take the shortcut and then expand the sniff once it's necessary to do so. |
Yes, they are :) |
Wonderful, thanks! |
Great. I'll refactor that part using the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This sniff will be really helpful (not only) for ORM 3.0, DBAL 3.0, Migrations 2.0 and other libraries.
I'd also love to see a sniff that validates actual throw
that we don't throw i.e. SPL exceptions. But that'd be separate task/sniff. :)
tests/Doctrine/Tests/Sniffs/Classes/ExceptionInterfaceNamingSniffTest.php
Outdated
Show resolved
Hide resolved
@Majkl578 I've updated the PR for the interface handling. |
use PHP_CodeSniffer\Files\File; | ||
use SlevomatCodingStandard\Helpers\UseStatementHelper as UseHelper; | ||
|
||
class UseStatementHelper |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
I'll try to provide a more detailed review in the afternoon, here are just a few issues I spotted:
If you want, I can provide some base for writing/testing sniffs, similar to how slevomat/coding-standard and cdn77/coding-standard are currently tested. Feel free to ping me on Slack if you want to discuss any of above or anything else related to the CS. 👍 |
]; | ||
} | ||
|
||
/** | ||
* @return string[][]|int[][] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How does the correct syntax for multidimensional arrays look like? Seems like the sniff is satisfied with multiple possible formats.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is correct. (string|int)[][]
would be more correct, but it is not supported by most tooling yet :-\
Addition to Exception classes: They have to be |
Disagree on exception classes being This means that each exception is required to implement at least 2 interfaces. |
I'd say the ideal solution would be something like this:
Interface-based hiearchy should be generally always preferred and usage of intermediate (abstract) exceptions should be rather exceptional.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did a more thorough review - let me know if something is not clear, reviewing sniffs isn't easy since different people choose different approaches for the same outcome. :)
private function parseExtendedInterfaces(File $phpcsFile, int $stackPtr) : array | ||
{ | ||
$limit = $phpcsFile->findNext([T_OPEN_CURLY_BRACKET], $stackPtr) - 1; | ||
$start = $phpcsFile->findNext([T_EXTENDS], $stackPtr, $limit); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use SlevomatCodingStandard\Helpers\TokenHelper::findNext($phpcsFile, [T_EXTENDS], $stackPtr + 1, $limit)
.
*/ | ||
private function parseExtendedInterfaces(File $phpcsFile, int $stackPtr) : array | ||
{ | ||
$limit = $phpcsFile->findNext([T_OPEN_CURLY_BRACKET], $stackPtr) - 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use SlevomatCodingStandard\Helpers\TokenHelper::findNext($phpcsFile, [T_OPEN_CURLY_BRACKET], $stackPtr + 1)
.
$limit = $phpcsFile->findNext([T_OPEN_CURLY_BRACKET], $stackPtr) - 1; | ||
$start = $phpcsFile->findNext([T_EXTENDS], $stackPtr, $limit); | ||
|
||
if ($start === false) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the changes above: === null
.
return []; | ||
} | ||
|
||
$extendedInterfaces = explode(',', $phpcsFile->getTokensAsString($start + 1, $limit - $start)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It'd be better to do this properly, using combination of SlevomatCodingStandard\Helpers\TokenHelper::findNextLocal($phpcsFile, [T_COMMA], $start, $limit)
+ SlevomatCodingStandard\Helpers\TokenHelper::getContent($phpcsFile, $start, $localEnd)
between $start
/comma and comma/$limit
(can be done with for
or do...whie
.
/** | ||
* @return string[] | ||
*/ | ||
private function parseExtendedInterfaces(File $phpcsFile, int $stackPtr) : array |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we move this to Helpers?
} | ||
|
||
$isImplementingExceptions = count(preg_grep('/Exception$/', $implementedInterfaces)) > 0; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should early-exit here if $isImplementingExceptions
is true.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the sniff for classes. Implementing an Exception interface is not enough for a class, because it needs to extend another Exception class too. I don't know about a requirement that an Exception class has to implement an interface.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Implementing an Exception interface is not enough for a class
That's true, but this sniff is about naming only, it should not report inheritance issues IMO.
Not extending some Exception will cause PHP fatal error, but that is out of scope for this sniff.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I even start to see no point here of checking for an exception interface when a class, regular or abstract, has to extend an exception. This is a relict from before making exception classes final
. I think the complexity of checks for exception interfaces can be removed here. How do you think about that?
|
||
$importedClassNames = UseStatementHelper::getUseStatements($phpcsFile); | ||
|
||
// TODO Should throwable be checked separately, because it can't be implemented on non-abstract exception class? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's not allow this at all - you can't implement Throwable without extending Exception/Error.
$hasValidClassName = ($isAbstract && $hasExceptionName) || | ||
(! $isAbstract && ! $hasExceptionName && $isFinal); | ||
$isValidException = $hasValidClassName && ($isExtendingException || $isImplementingException); | ||
$isNoException = ! $hasExceptionName && ! $isExtendingException && ! $isImplementingException; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please try to turn these combinations into multiple early-exit if
s, this is too complex.
|
||
if (! $hasValidClassName) { | ||
$phpcsFile->addError( | ||
'Use "Exception" suffix for abstract exception classes and make non-abstract classes final', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exception classes must end with an "Exception" suffix. (should not mention abstract/final here since we check for class name)
} | ||
|
||
$phpcsFile->addError( | ||
'Class is not a valid exception', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should say why: Exception classes must be either abstract or final.
Not ready yet, but I'm almost done. |
Ready for another review. |
phpcs.xml.dist
Outdated
@@ -13,7 +13,8 @@ | |||
<!-- Show progress of the run --> | |||
<arg value="p"/> | |||
|
|||
<rule ref="Doctrine"/> | |||
<rule ref="DoctrineCodingStandard"/> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is quite the BC break - are we sure we have to/want to rename the standard?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I run into issues and chose the second solution mentioned:
#63 (comment)
But I admit, I didn't use much time to test how to make it run with the previous namespace.
|
||
final class StillNoExtension implements FooException | ||
{ | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some of these files don't contain a trailing EOL. It's either missing from the sniff or we don't properly check tests. Either way, there should be a consistent rule on this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These tests are only for the new exception sniffs to prevent failing tests on new additions in future PRs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean the trailing newlines - this is part of PSR-2 and should be fixed here as well.
public static function isImplementingThrowable(array $importedClassNames, array $implementedInterfaces) : bool | ||
{ | ||
return (in_array(Throwable::class, $importedClassNames, true) && | ||
in_array(Throwable::class, $implementedInterfaces, true)) || |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to make sure, I assume this could lead to issues if Throwable
is aliased?
use My\Namespace\Throwable;
use Throwable as BaseThrowable;
class Exception implements Throwable
We might want to also fetch aliases from use
statements and explicitly check against that to avoid wrong results.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess the whole thing depends on whether findImplementedInterfaceNames
returns aliased names or the actual FQCN. If it's the latter, this works as expected.
FWIW, there's a typo in TrowableAlias
in your example that I just spotted now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch. It looks like it doesn't matter for the functionality, but names in code are important to me.
This PR should also get an addition to the documentation for the new rules. |
phpcbf seems to change the docblock of tests/input2/test-case.php from
to
The Apply fixes stage build fails. (PR #91 handles this case) |
The code also is used to disable sniffs in projects. Therefore changing a code constant would be a BC break and should be catched by the test.
I've rebased to the current branch to keep this PR up to date. |
I agree with @Ocramius, not liking idea about any exception classes being final. On different note, even though this PR seems like great work, it does too much rules at once, which is why people hesitate to review it. Having less rules at least initially would allow less disagreements and easier time to review and merge it. |
@SenseException do you believe this to be ready for review or still WIP? |
I would prefer reviews for the changes @Majkl578 and I did. Feedback can help development on this PR. It does a lot of stuff and there might be still some uncovered cases. The question about having |
I would prefer reviews before I rebase against master. |
{ | ||
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 comment
The 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 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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
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 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
There was a problem hiding this comment.
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.
Deferred to |
What is the status of this PR? |
I'm currently not working on this WIP and don't know when I'll come back to it. |
I've started to create sniffs for exception naming as described in the readme:
Abstract exception class names and exception interface names should be suffixed with Exception
.This PR isn't a finished one yet and contains the first draft for naming exception interfaces. As this is about static code analysis, I had to make some assumptions about the parsed code.
Interfaces
*Exception
, I assume, that the interface is an exception interface and needs to be handled by the sniff.\Throwable
, it is definitely an exception and needs the*Exception
suffix.This one is covered for most cases with the creation of the PR. There are still forms of interfaces I need to cover and the sniff isn't finished either, but there's still room for feedback on the existing code.
The sniff is currently not covering
use
cases, where class alias, comma separation and group use declaration is used, but the dependency slevomat/coding-standard has helper classes likeUseStatementHelper
, that can help parsing those. Should I use the classes of this dependency for doctrine's custom coding standard sniffs or should I not introduce code from dependencies there?Classes
This still needs to be handled with this PR.*Exception
, I assume, that the class is an exception class and needs to be handled by the sniff.*Exception
in its name, I assume the class is an exception class and needs its name checked.Question
Should an abstract class only extend other abstract exception classes and basic PHP exception classes? Otherwise how do I know that an extended class without
*Exception
in its name is a valid regular exception class as the coding standard allows? Should I check if the class is in an exception namespace? Does the standard expect all exceptions in an exception namespace?Addendum: leave exception classes have to be final. Exception namespaces are not in the scope of this PR.