-
-
Notifications
You must be signed in to change notification settings - Fork 69
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
Ruleset: improve error handling #857
Conversation
$reflMethod = new ReflectionMethod($ruleset, 'displayCachedMessages'); | ||
$reflMethod->setAccessible(true); | ||
$reflMethod->invoke($ruleset); | ||
$reflMethod->setAccessible(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.
Do we need to set this "back" to accessible=false? The object ($reflMethod
) will be cease to exist immediately after this line of code.
$reflMethod->setAccessible(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.
Force of habit just in 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.
Thanks for working on this, @jrfnl. This will be a great improvement for the user experience.
Overall, it looks good to me, and it works as expected. I left a few comments with questions and minor suggestions.
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.
Thanks for checking my suggestions, @jrfnl. Everything looks good to me now.
As there have not been any new comments for two days, I'm going to rebase this PR and squash the "fix up" commits into their originating commits. Once the build has passed again, I'll be merging this PR. |
This _internal-use only_ `PHP_CodeSniffer\Util\MessageCollector` class can be used to collect various type of "error" messages, including notices, warnings and deprecations for display at a later point in time. This class is intended to be used by various steps in the PHPCS process flow to improve the error handling and prevent the "one error hiding another" pattern. Design choices: * This class does not have a constructor and has no awareness of the applicable `Config` or other aspects of a PHPCS run. If notices should be displayed conditionally, like only when not in `-q` quite mode, this should be handled by the class _using_ the `MessageCollector`. * While duplicate error messages are not a great user experience, this class allows for them. It is the responsibility of the class _using_ the `MessageCollector` to ensure that the messages cached are informative for the end-user. * The class provides a number of (`public`) class constants to indicate the error level when adding messages. It is _strongly_ recommended to only ever use these class constants for the error levels. The default error level for messages will be `NOTICE`. Note: yes, these class constants should basically be an Enum, however, enums are a PHP 8.1+ feature, so can't be used at this time. * The class provides two (`public`) "orderby" class constants to indicate the display order of the messages. It is _strongly_ recommended to only ever use these class constants for the display order. By default, messages will be ordered by severity (with "order received" being the secondary ordering for messages of the same severity). Note: again, yes, these class constants should basically be an Enum. * When display of the messages is requested and the message cache includes messages with severity `ERROR`, the class will throw a RuntimeException, which will cause the PHPCS run to exit with a non-zero exit code (currently `3`). * In all other cases, the messages will be displayed, but the PHPCS run will continue without affecting the exit code. * It was suggested by fredden to consider implementing [PSR-3: Logger Interface](https://www.php-fig.org/psr/psr-3/) for the `MessageCollector`. While we discussed this, I don't consider this necessary at this time, though as the class has no BC-promise, this can be reconsidered in the future. Arguments against implementing PSR-3 at this time: - Placeholder handling is required in the PSR-3 implementation to allow for logging to different contexts and using appropriate variable escaping based on the context. For PHPCS, messages will only ever be displayed on the command-line, so there is no need to take escaping for different contexts into account. - PSR-3 expects handling of eight different severity levels - emergency, alert, critical, error, warning, notice, info, debug -. For PHPCS, the `emergency`, `alert` and `critical` levels, as defined in PSR-3, are redundant as these will never occur. Along the same line, PHPCS does not log `info`. However, we do want to be able to display deprecation notices, but that is a severity level not supported under PSR-3. The new functionality is fully covered by tests. Potential future scope: * Add a constructor and inject the `Config` class to allow for colorizing the messages/message prefixes if color support is enabled. This would be most effective after issue 448 has been addressed first. * Add a new `CRITICAL` level for errors which prohibit the current task from finishing. When an error with such level would be received, it would cause the class to display all messages collected up to that point straight away via an exception. * If the conditional displaying of the messages, like only when not in `-q` mode, would lead to undue duplicate code, potentially move display conditions handling into the `MessageCollector` class. This would also need the aforementioned constructor with `Config` injection.
This commit adds a mechanism to handle errors encountered while loading a ruleset in a more user-friendly manner. * Errors and notices aimed at end-users and ruleset maintainers will be collected while processing the ruleset. * Only once the complete processing of the ruleset is finished, will all errors/notices be displayed. This prevents "one error hiding behind another". * If there are only notices (deprecations/notices/warnings), these will not display when running `-e` (explain), `-q` (quiet mode) or `--generator=...` (documentation). * Errors will always display and will hard exit out of the run with a non-zero exit code. This implementation should be seen as an interim - "good enough for now" - solution, which can be iterated on in the future. I can imagine a more code-base wide solution at some later point in time, but that should not block this initial improvement. As the current implementation doesn't change the public API (the only new methods are `private`), it should be feasible to transform the current solution to whatever form a future solution will take without breaking changes. Includes tests covering the new functionality.
Notes: **For the "invalid type" message** Includes updating the message for the "invalid type" message to mention the reference for which the `type` was (incorrectly) being changed. This should make it more straight forward for ruleset maintainers to find the problem in their ruleset. It also makes the message more unique, as this message could occur in multiple places in a ruleset and there was no indication of that in the message previously. Potential future scope for "invalid type" message It could be considered to downgrade this message from an `ERROR` to a `NOTICE` as an invalid type is not blocking for running the sniffs, though this could lead to results not being as expected if, for instance, the `-n` flag is being used, which is why I've not changed this at this time. **For the "register() method must return an array" error Includes some new assertions which won't run until the test suite supports PHPUnit 10+ (PHPCS 4.0). These tests belong with this commit though, so adding them now anyway. **For the "setting non-existent property" error Includes minor adjustment to the error message (removal of "Ruleset invalid" and punctuation).
7af7ca9
to
45baf5c
Compare
Description
👉🏻 This PR has been a while in the making. While I'm confident about the principle of it, I've been struggling to write up the implementation choices made, which normally means there is still something not completely "right".
So under the guise of "Perfect is the enemy of good", here goes anyway (as this PR is blocking everything else I'm working on, as well as blocking the 3.12.0 release).
So please consider this a first iteration. The actual implementation details can be iterated on both in this PR, as well as via future PRs.
This PR is intended to solve two problems for the
Ruleset
class and to allow for implementing the same kind of solution in other parts of the codebase at a later point in time.Current Situation
Up to now, the
Ruleset
class only allowed for hard errors which would exit the run immediately with a non-zero exit code.Problem 1: The Ruleset class only allows for hard errors
In issue 689, the need to be able to display deprecation notices when processing rulesets was identified.
These deprecation notices should be displayed in a user-friendly manner and should not affect the exit code of PHPCS, nor stop PHPCS from continuing its run.
Problem 2: The Ruleset class provides errors to the user one by one
If a Ruleset contains multiple errors, running PHPCS would only display one error at a time, which means the user would have to:
This pattern of showing errors piecemeal (one-by-one) is not terribly user-friendly, so the intention is to change this behaviour.
What this PR changes
This PR introduces a new
++MsgCollector
MessageCollector
++ class which is intended to fix both problems. Errors and notices are "written to" the++MsgCollector
MessageCollector
++ while processing theRuleset
and only displayed when theRuleset
has finished its processing.If there are errors amongst the collected messages, PHPCS will still stop and exit with a non-zero exit code at that point.
If only warnings/notices/deprecations were collected, PHPCS will display these and then continue onto the next step in the PHPCS process.
👉 Note: At this time, no changes are being made to the error "level" of the pre-existing errors coming from the
Ruleset
class.Also note that the new
++MsgCollector
MessageCollector
++ class is marked as a PHPCSinternal
class and has no BC-promise to allow for iterating on the solution.Future Scope
At a later point in time, this same principle can be applied in more places.
In particular I'm thinking of:
Config
class (in conjunction with Config: read all CLI args before doing a deep exit #448). This would then also allow for deprecation of CLI args if/when needed.Fixer
class to fix issue Runtime exception originating from sniff fixers are not handled squizlabs/PHP_CodeSniffer#2871 (PHP errors encountered during fixing are not reported leading to potentially incomplete results without the user being informed).Both of these improvements are currently blocked by the lack of tests for those parts of the codebase.
Implementation choices made for this PR
A this time, the
Ruleset
class throws the following errors:Details about the errors thrown from the `Ruleset` class (with context)
I've categorized these errors in the above list as "ruleset errors" and "sniff developer errors".
For now, I'm going to leave the sniff developer errors alone. An end-user should never run into these and we may want to have a think about how to ensure that even better, but for now, in my opinion, that is outside of the scope of this ticket.
So that leaves us with the following errors:
While I'd like to collect ALL of these errors, these is one for which an exception MUST be made:
A missing autoload file may or may not be problematic in the context of the
++MsgCollector
MessageCollector
++, depending on whether whichever classes are supposed to be loaded via the custom autoloader are actually used by the sniffs being loaded in the ruleset and when.Let me explain. There are basically three possible situations:
process()
method and the sniff may bow out before ever reaching the code which calls the class which needs autoloading. In that case, it depends on the "code under scan" whether the missing autoload file would be problematic.extends Something
orimplements Something
.In that case, loading the sniff file while reading the ruleset (as done in Ruleset::registerSniffs() line 1337 -
$className = Autoload::loadFile($file);
), would lead to a "Class not found" error and this error can only be caught since PHP 7.3, which means there is no way we can catch this error in a PHP cross-version compatible manner for all currently supported PHP versions.So, it is this last case which is the blocker. If the "autoload file missing" error would be collected via the
++MsgCollector
MessageCollector
++, any subsequent "Class not found" errors, encountered while processing the rulesets, would need to be handled and passed off to the++MsgCollector
MessageCollector
++ as well, but we can't currently do that for all supported PHP versions, which means we can't "collect" the "autoload file missing" error.And if we're going to make exceptions anyway, I'm inclined to not "collect" the "ruleset XML parse error" error either.
While we could "collect" it, ignore the problematic (sub-)ruleset and continue processing other rulesets, I kind of consider an XML parse error serious enough to error straight away.
👉 Note: I'm open to being persuaded to "collect" this error anyway.
Alternative to not "collecting" these errors
As an alternative, I considered "collecting" those errors anyway and adding an extra severity level (
CRITICAL
) to the++MsgCollector
MessageCollector
++.In that case, if a message with severity
CRITICAL
would beadd
ed, the++MsgCollector
MessageCollector
++ woulddisplay()
whatever had been collected straight away.The upside of this is that any messages collected prior to the
CRITICAL
error would also be displayed (instead of getting lost/displayed only in the next run after the critical error was fixed).The downside of this is that the
++MsgCollector
MessageCollector
++ would be less context agnostic (less "clean") and would contain logic deciding when to display the messages instead of leaving that to the class using the++MsgCollector
MessageCollector
++.For this first iteration, I've decided against this alternative solution, but as the
++MsgCollector
MessageCollector
++ is marked as "internal" and has no BC promise, this change could still be made at a later time.Commits
Note: the last five (six) commits should be squashed into a single commit before merging. They are currently separate commits only to make it more straight-forward to review this PR.
✨ New
++MsgCollector
MessageCollector
++ utility classThis internal-use only
PHP_CodeSniffer\Util\MessageCollector
class can be used to collect various type of "error" messages, including notices, warnings and deprecations for display at a later point in time.This class is intended to be used by various steps in the PHPCS process flow to improve the error handling and prevent the "one error hiding another" pattern.
Design choices:
Config
or other aspects of a PHPCS run.If notices should be displayed conditionally, like only when not in
-q
quite mode, this should be handled by the class using the++MsgCollector
MessageCollector
++.It is the responsibility of the class using the
++MsgCollector
MessageCollector
++ to ensure that the messages cached are informative for the end-user.public
) class constants to indicate the error level when adding messages.It is strongly recommended to only ever use these class constants for the error levels.
The default error level for messages will be
NOTICE
.Note: yes, these class constants should basically be an Enum, however, enums are a PHP 8.1+ feature, so can't be used at this time.
public
) "orderby" class constants to indicate the display order of the messages.It is strongly recommended to only ever use these class constants for the display order.
By default, messages will be ordered by severity (with "order received" being the secondary ordering for messages of the same severity).
Note: again, yes, these class constants should basically be an Enum.
ERROR
, the class will throw a RuntimeException, which will cause the PHPCS run to exit with a non-zero exit code (currently3
).++MsgCollector
MessageCollector
++.While we discussed this, I don't consider this necessary at this time, though as the class has no BC-promise, this can be reconsidered in the future.
Arguments against implementing PSR-3 at this time:
For PHPCS, messages will only ever be displayed on the command-line, so there is no need to take escaping for different contexts into account.
For PHPCS, the
emergency
,alert
andcritical
levels, as defined in PSR-3, are redundant as these will never occur.Along the same line, PHPCS does not log
info
.However, we do want to be able to display deprecation notices, but that is a severity level not supported under PSR-3.
The new functionality is fully covered by tests.
Potential future scope:
Config
class to allow for colorizing the messages/message prefixes if color support is enabled.This would be most effective after issue Config: read all CLI args before doing a deep exit #448 has been addressed first.
CRITICAL
level for errors which prohibit the current task from finishing. When an error with such level would be received, it would cause the class to display all messages collected up to that point straight away via an exception.-q
mode, would lead to undue duplicate code, potentially move display conditions handling into the++MsgCollector
MessageCollector
++ class.This would also need the aforementioned constructor with
Config
injection.Ruleset: wire in
MsgCollector++MessageCollector++This commit adds a mechanism to handle errors encountered while loading a ruleset in a more user-friendly manner.
This prevents "one error hiding behind another".
-e
(explain),-q
(quiet mode) or--generator=...
(documentation).This implementation should be seen as an interim - "good enough for now" - solution, which can be iterated on in the future.
I can imagine a more code-base wide solution at some later point in time, but that should not block this initial improvement.
As the current implementation doesn't change the public API (the only new methods are
private
), it should be feasible to transform the current solution to whatever form a future solution will take without breaking changes.Includes tests covering the new functionality.
Ruleset: use
MsgCollector++MessageCollector++ for "no sniffs registered" errorRuleset: use
MsgCollector++MessageCollector++ for "referenced sniff not found" errorRuleset: use
MsgCollector++MessageCollector++ for "invalid type" messageIncludes updating the message to mention the reference for which the
type
was (incorrectly) being changed.This should make it more straight forward for ruleset maintainers to find the problem in their ruleset.
It also makes the message more unique, as this message could occur in multiple places in a ruleset and there was no indication of that in the message previously.
👉🏻 This commit will be easiest to review while ignoring whitespace differences.
Potential future scope
It could be considered to downgrade this message from an
ERROR
to aNOTICE
as an invalid type is not blocking for running the sniffs, though this could lead to results not being as expected if, for instance, the-n
flag is being used, which is why I've not changed this at this time.Ruleset: use
MsgCollector++MessageCollector++ for "register() method must return an array" errorIncludes some new assertions which won't run until the test suite supports PHPUnit 10+ (PHPCS 4.0). These tests belong with this commit though, so adding them now anyway.
Ruleset: use
MsgCollector++MessageCollector++ for "setting non-existent property" errorIncludes minor adjustment to the error message (removal of "Ruleset invalid" and punctuation).
Suggested changelog entry
Whenever possible, the ruleset processing will be allowed to run to its conclusion before displaying all ruleset errors in one go.
Previously an error in a ruleset would cause PHPCS to exit immediately.
Related issues/external references
Related to #689 (comment)
Types of changes