-
-
Notifications
You must be signed in to change notification settings - Fork 523
First pass at sniff to confirm inline docs for filters are present #1892
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
|
Hi @kraftbj Welcome to WPCS ;-) Thanks for this PR. I haven't looked at the code yet, but would like to ask you whether you have run the sniff over WP Core to see what it throws up. Any false positives from WP Core should be handled, or when in doubt whether something is a false positive, let's discuss it.
Core requires three levels too as per the Documentation guidelines, so checking for three levels is fine. FYI: I expect WPCS 3.0.0 to contain a trait for version number checking (written for another sniff), so that can probably be re-used. With that in mind and the last 2.x release expected soon, after which the pulls for WPCS 3.0.0 will start, I'd like to earmark this PR for WPCS 3.0.0. |
jrfnl
left a comment
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.
Checklist of things to do before this PR will be reviewed:
- Move the sniff to
WordPress/Sniffs/Commenting.
TheCommentingsubdirectory name is common for documentation sniffs in the PHPCS-sphere. - Add unit tests.
Optional, but highly recommended:
- Add XML documentation for the sniff to the
WordPress/Docsdirectory.
Suggested:
- Have a look at the
Sniff::$hookInvokeFunctionsproperty and see about using it.
|
Sounds good. New to PHPCS overall, so appreciate the guidance on naming scheme. Took a guess that I'll get some unit tests worked up early next week since it sounds like this is a sniff that would be considered for inclusion. |
Please feel free to ask for as much guidance as you need from me and the others in the team. In my case, my remarks will often presume you know what you're doing with PHPCS (as why would you send in a sniff otherwise?), please don't let that intimidate you and ask for as much clarification as you need.
Definitely. There are long standing open issues about this check missing from WPCS, though I also have to caution you that it may not be the easiest issue to tackle. Can be a great way to learn more about writing sniffs though ;-) |
|
Sounds good. I appreciate the tip on running it against WP to find false positives. I'll make a couple of tweaks when sending up the unit tests. |
Maybe The PR is going to need some unit tests as well. |
I don't think we should allow that. Both SVN as well as Git allow to trace things through history, so I can't think of a valid reason to allow |
|
I added unit tests and documentation, but I'm not sure how to unit test when the ruleset would be passing a property as an array. With the X.Y.Z. format, there are two instances currently in Core which does not follow this which I am not keen at automatically changing:
To allow these, I set a rule property of an array that could be defined in the ruleset XML for any "exceptional" versions. I just don't know how to pass that to the unit test via Any pointers? |
For Core, I would disagree, but for implementing projects, I did add a way to set an "exceptional" version. If an implementing project wanted to allow that, they could in their own |
See: https://github.com/WordPress/WordPress-Coding-Standards/blob/develop/WordPress/Tests/NamingConventions/PrefixAllGlobalsUnitTest.1.inc#L6 for an example of setting an array property. Please also remember to reset the property after the test using it and/or at the end of the test file (see bottom of the above linked test file for an example). |
44052ec to
4a49c10
Compare
4a49c10 to
d4d4ed0
Compare
|
@jrfnl I really appreciate the pointers and direction. I think this is at a state where you can |
|
@kraftbj Wowie! I'll try and have a look some time this week ;-) |
GaryJones
left a comment
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.
Left a few comments for you to consider.
| * | ||
| * @var array WordPress hook execution function name => filter or action. | ||
| */ | ||
| protected $hook_functions = 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.
We have a lot of arrays of functions names in the Sniff class that all of the Abstract (and therefore concrete) classes extend from - might be worth renaming the property to a bit more explicitly ($hook_executing_functions?) and then moving them to there so then other sniffs that need this collection can use them.
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.
Nice pointer. In 11e3a8f, I removed my array. There is already a $hookInvokeFunctions array in the Sniff class.
The difference is the value of each key in the existing one is true whereas mine had filter|action. Originally, I was going to ensure that the "previously documented" check (This filter is documented) used filter or action correctly, but opted against being that nit-picky.
Since I wasn't used the key, let's just use what's already there.
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 the existing values were all true, then chances are the values weren't being used elsewhere. If there is benefit in having them be either 'filter' or 'action' so that this sniff can check a little more finely (would you propose to throw a different violation if they didn't match?), then I'm all for that.
cc @jrfnl for consideration. Unresolving this comment so it doesn't get missed.
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 pretty sure that the values in the array aren't currently used, but a search of the codebase can verify this for sure.
I'm all for it to have filter|action as the values. The error messages could then potentially also use that term if it makes the error message clearer.
| */ | ||
| if ( ( $this->tokens[ $previous_comment ]['line'] + 1 ) !== $this->tokens[ $stack_ptr ]['line'] ) { | ||
| $this->phpcsFile->addError( | ||
| 'The inline documentation for a hook must be on the line immediately before the function call.', |
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.
As noted previously, this may not always be possible. Consider this example from Juliette:
/**
* Hook docblock
*/
if ( $a === true
&& apply_filter('some_filter', $value) > 10
) {
// Do something.
}Likewise, this should also be valid IMO, since it matches the PSR-5 definition of associating a DocBlock with a Structural Element with an optional number of clear lines in between:
/**
* Hook docblock
*/
$value = apply_filters( 'some_filter', $value );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 concur. And also consider a call to a filter hook nested in another function call, which may be multi-line.
Think:
$foo = function_call(
$param,
array( 1, 2, 3 ),
apply_filters( 'hook_name', $something)
);Other things to consider:
- PHPCS ignore comments, those should be accepted between the docblock and the code.
- What if the comment found is a trailing comment for a previous code line ?
I suggest having a good read through the hook documentation rules to get this right. pay special attention to:
If a hook is in the middle of a block of HTML or a long conditional, the DocBlock should be placed on the line immediately before the start of the HTML block or conditional, even if it means forcing line-breaks/PHP tags in a continuous line of HTML.
From a code perspective, this should probably be a "Missing hook docblock" error and not complain about the line at all.
The way things are coded now, the "previous comment" might well be the docblock for a function declaration further up in the code or some other completely unrelated comment.
And if the hook docblock is "missing", the sniff should bow out from doing the rest of the checks as it may be examining a comment completely unrelated to the hook.
| // "Already documented" example of a filter. | ||
| /** This filter is documented in file.php */ | ||
| apply_filters(' hook', 'function' ); | ||
|
|
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 are good, but some real-world edge cases will be needed!
Consider:
/**
* This is my function DocBlock.
*
* @since 0000.00000.0000000000
*/
function my_function_has_a_hook() {
do_action( 'foo' );
}Likewise:
/**
* This is my function DocBlock.
*
* @since 0000.00000.0000000000
*/
function my_function_has_a_hook() {
echo 'foo';
}
do_action( 'foo' );If these both already get reported as missing the hook DocBlock, then great! Add them in as unit tests, and then try:
/**
* This is my function DocBlock.
*
* @since 0000.00000.0000000000
*/
function my_function_has_a_hook() { do_action( 'foo' ); }😈
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.
The first two did catch "correctly", but probably wouldn't after I resolve the preceding line issue mentioned in #1892 (comment)
But, the third example of a single-line function fails the unit test since it doesn't see that as an error.
I'm going to push up what I have (and mark this as resolved since the tests are added) and think on it a bit.
I'm thinking for the third example, we can search for a comment previous to the hook but stop at a function declaration at least. May be others.
Thanks!
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.
Either here or PHPCSUtils, I bet @jrfnl has examples / helper functions to find something within a specific scope i.e. find a DocComment within the current scope.
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 do have docblock finder helper functions lined up to go into PHPCSUtils at some point, but as hook docblocks are a bit of a different beast compared to most structural elements I don't think that will help here.
I do have a draft HooksTrait lined up for WPCS 3.0.0 with logic for finding hook docblocks (for another sniff).
When I pull that, I will combine the logic for finding hook docblocks from this sniff with the logic in the trait to get the best of both brains in the trait (and switch this sniff over to using the trait).
| ]]> | ||
| </code> | ||
| <code title="Invalid: using a non-docblock comment"> | ||
| <code title="Invalid: not using a DocBlock comment"> |
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.
A "DocBlock" is made up of two parts:
- the
/** */structure, known as a DocComment. - the contents with Summary, optional description, tags, etc. known as the PHPDoc.
As such, the "DocBlock comment" here, which is referencing whether one asterisk or two is used, is really checking if it's a multi-line comment (/*) or a DocComment (/**), hence my earlier suggestion of `Invalid: not using a DocComment."
(Note that the period is still missing at the end from your version.)
jrfnl
left a comment
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.
Hi @kraftbj, thanks for your patience.
I've done an initial review of the sniff and have left some (lots...) of comments inline. Please don't be discouraged by that. A lot of comments are only minor things, though the docblock finding logic does still need further refinement.
Overall, this is not bad at all for a first pass at writing sniffs/writing this sniff. 👍
Other than that:
- Sniff name: let's have a little discussion about the name:
Basically within the WP context, hook invocations are a structural element, just like class or function declarations, which is why a docblock is required.
The sniff adheres to that and explicitly does not allow inline comment-style, but expressly looks for docblocks and will only accept those.
So with that in mind, I personally find the sniff name containingInlineDocsconfusing.
Suggestion for an alternative name:HookDocBlockSniff.
I'm open to other names if someone comes up with a better one.
Note: when you do change this, please change it everywhere the name is used, including in the docs. - Ruleset: the sniff will currently only be included if people include the
WordPressruleset, which automatically includes all sniffs.
I think it would be a good idea to actually add the sniff to the appropriate rulesetWordPress-Docs. - Please do at least minimal validation of the received property input. I.e. make sure it is an array which was passed and filter out potential empty values before using the property.
- The sniff class and function docblocks should all have a
@sincetag. (For the unit tests this is only needed for the class docblock, where it is set correctly.) You can use3.0.0as the version number.
Hope my comments help you to further iterate on the sniff. Let me know if you have any questions.
| * By default, X.Y.Z version numbers are required. If there are any exceptions, | ||
| * they can be passed in the ruleset XML file via: | ||
| * <rule ref="WordPress.Commenting.HooksInlineDocs"> | ||
| * <properties> |
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 not how array values are declared in a ruleset. Aside from the XML parse error, this also won't work if used per the example.
Try this instead:
<rule ref="WordPress.Commenting.HooksInlineDocs">
<properties>
<property name="allowed_extra_versions" type="array">
<element value="0.71"/>
<element value="MU (3.0.0)"/>
</property>
</properties>
</rule>| /** | ||
| * Groups of functions to restrict. | ||
| * | ||
| * Example: groups => 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.
Please remove the example from the abstract. This property is not intended to be overruled.
| use PHP_CodeSniffer\Util\Tokens; | ||
|
|
||
| /** | ||
| * Class HooksMustHaveDocblockSniff |
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 add a proper description of what the sniff is about and what it is trying to detect.
Include links to the handbook where relevant.
| * @package WPCS\WordPressCodingStandards | ||
| */ | ||
| class HooksInlineDocsSniff extends AbstractFunctionRestrictionsSniff { | ||
| /** |
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.
Nitpick: one blank line after the class declaration line please (also for the unit tests).
| ), | ||
| ); | ||
| } | ||
|
|
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 add a process_token() method to prevent an exclude property (inherited from the parent sniff) which can be set in a custom ruleset from disabling this sniff.
Something like this:
/**
* Processes this test, when one of its tokens is encountered.
*
* @since x.x.x
*
* @param int $stackPtr The position of the current token in the stack.
*
* @return int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
public function process_token( $stackPtr ) {
// Disallow excluding function groups for this sniff.
$this->exclude = array();
parent::process_token( $stackPtr );
}| if ( '@since' === $this->tokens[ $tag ]['content'] ) { | ||
| $has['since'] = true; | ||
| // Find the next string, which will be the text after the @since. | ||
| $string = $this->phpcsFile->findNext( T_DOC_COMMENT_STRING, $tag, $comment_end ); |
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 doesn't do what you expect/documented.
Test case:
/**
* Summary.
*
* @since
*
* @param int $value Description.
*/As it is, it will find the comment string related to the parameter. You account for that by doing a line check, but the sniff would be more efficient by doing a reverse find (find the next non-docblock-whitespace token and checking it's a T_DOC_COMMENT_STRING token). You'll still need the same line check though.
| } | ||
| } | ||
| } | ||
| } |
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 (early) continue/breaks could make this whole foreach block a lot more readable.
| // If it is false, there is no text or if the text is on the another line, error. | ||
| if ( false === $string || $this->tokens[ $string ]['line'] !== $this->tokens[ $tag ]['line'] ) { | ||
| $this->phpcsFile->addError( 'Since tag must have a value.', $tag, 'EmptySince' ); | ||
| } elseif ( ! preg_match( '/^\d+\.\d+\.\d+/', $this->tokens[ $string ]['content'], $matches ) ) { // Requires X.Y.Z. Trailing 0 is needed for a major release. |
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.
Don't declare $matches if you're not going to use it. (though using it in a Found: %s might not be a bad idea.)
Also be aware that \d is locale dependent.
| } elseif ( ! preg_match( '/^\d+\.\d+\.\d+/', $this->tokens[ $string ]['content'], $matches ) ) { // Requires X.Y.Z. Trailing 0 is needed for a major release. | |
| } elseif ( preg_match( '/^\d+\.\d+\.\d+/', $this->tokens[ $string ]['content'] ) !== 1 ) { |
| * | ||
| * @return bool If any array key begins within the given string. | ||
| */ | ||
| protected function array_begins_with( $string, $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.
This could be a utility function if other sniffs would have a use for it. As it is, it is a sniff local function and only used for one specific check, so you may as well refer directly to the class property instead of passing the $array in.
Also the method name could probably be more descriptive.
| apply_filters( 'hook', 'function' ); | ||
|
|
||
| // phpcs:set WordPress.Commenting.HooksLineDocs allowed_extra_versions[] | ||
|
|
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 missing a test case to confirm that the sniff throws an error for each @since tag with an incorrect/missing version in a docblock.
/**
* Summary.
*
* @since
* @since 1.2.3
* @since Some description.
*/
apply_filters(' hook', 'function' );The sniff should (and will) throw an error for the first and the third @since tag.
Also missing a test cases with a "more than three-#" type version nrs. The sniff as-is will handle this correctly, but we should safeguard that it does for the future.
/**
* Summary.
*
* @since 1.2.3.4
* @since 1.2.3-RC1
*/
apply_filters(' hook', 'function' );|
This is wonderful. Thank you so much for the review. Today is a standard holiday in the U.S. so a short week for me. I'll try to make a good dent on this during the week, but may get bumped to next. If so, my delay doesn't mean I'm discouraged! 🙂 |
|
While it would be nice to have a sniff for this, the PR as-is is not mergable with way too many open review comments and no action taken on them in over two years, so I'm going to close this PR. |
Fixes #424
First pass at sniff to confirm inline docs for hooks are present.
I'm adding this into Jetpack via Automattic/jetpack#15693 since I wanted to check for some Jetpack-specific things. Per suggestion, wanted to offer the "base" to core WPCS for everyone's benefit.
In the initial commit, it finds a matching the various hook function (L27) using WPCS'
AbstractFunctionRestrictionsSniffand checks for a preceding comment. The preceding comment need to be on the previous line (noting this is an open question from the Jetpack PR Automattic/jetpack#15693 (review)) and confirm it is using the docblock format.For the initial commit, it is verifying it has a
@sincetag within the docblock and that the value starts with a X.Y.Z version number. I'm not sure about WP itself, but Jetpack requires three levels to both confirm the exact version (e.g. 8.5 vs 8.5.0) and ensure that all hooks in the same major release (X.Y) will have the same value to avoid our docs parser tagging it as two separate versions.If Core WPCS doesn't need that, I can remove that to just verify a
@sincetag is present and move the version check itself to Jetpack as I did with our@moduletag in https://github.com/Automattic/jetpack/pull/15693/files#diff-eb7bc4afcdf23e6f99ae1f793afd119bR57