-
Notifications
You must be signed in to change notification settings - Fork 37
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
Feature/templates/required #214
base: develop
Are you sure you want to change the base?
Changes from all commits
355f42f
f6dc4f2
f521b85
6c4c1bc
3226883
c19ea43
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,233 @@ | ||||||
<?php | ||||||
/** | ||||||
* WPThemeReview Coding Standard. | ||||||
* | ||||||
* @package WPTRT\WPThemeReview | ||||||
* @link https://github.com/WPTRT/WPThemeReview | ||||||
* @license https://opensource.org/licenses/MIT MIT | ||||||
*/ | ||||||
|
||||||
namespace WPThemeReview\Sniffs\Templates; | ||||||
|
||||||
use PHP_CodeSniffer\Sniffs\Sniff; | ||||||
use PHP_CodeSniffer\Files\File; | ||||||
use PHP_CodeSniffer\Util\Tokens; | ||||||
|
||||||
/** | ||||||
* Ensures functions are called within HTML tags. | ||||||
* | ||||||
* Ex: <body <?php body_class(); ?>> | ||||||
* | ||||||
* @link https://make.wordpress.org/themes/handbook/review/required/#templates | ||||||
* | ||||||
* @since 0.2.0 | ||||||
*/ | ||||||
class RequiredFunctionSniff implements Sniff { | ||||||
|
||||||
/** | ||||||
* Sniff Settings | ||||||
* | ||||||
* @var array | ||||||
*/ | ||||||
public $tagsConfig = array( | ||||||
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.
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. Well, I don't know the stance on that for this repo, but it was intentional mostly for my own use case, which probably isn't very common. I figured if I had a need to tweak something, then someone somewhere might as well. I did name this as a "config" to indicate it was able to be configured and is an open point, so others can implement as needed. I generally like to leave configurations open as much as possible because you never know what someone might want to modify 😄, but I completely understand if that's not a standard approach here. 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. @timelsass Sorry, but this won't work. Please make this property While you can have If you'd want to be able to extend the array I suggest making it a Other than that, I think modular error codes is the way to go and you're already using those, so 👍 . |
||||||
'body' => array( | ||||||
'function' => 'body_class', | ||||||
'attribute' => 'class', | ||||||
), | ||||||
'html' => array( | ||||||
'function' => 'language_attributes', | ||||||
'attribute' => 'lang', | ||||||
), | ||||||
); | ||||||
|
||||||
/** | ||||||
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. This whole property can be removed. The PHPCS default is |
||||||
* Supported Tokenizers | ||||||
* | ||||||
* Currently this sniff is only useful in PHP as the required | ||||||
* functions to call are done in PHP. In testing various | ||||||
* themes - some had inline comments including `<html>`, and | ||||||
* were tokenized as T_INLINE_HTML throwing some false positives. | ||||||
* | ||||||
* @var array | ||||||
*/ | ||||||
public $supportedTokenizers = array( 'PHP' ); | ||||||
|
||||||
/** | ||||||
* Tag being searched. | ||||||
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. This description needs to be expanded as from the description alone, it is unclear what the property is for. Also: should this property maybe be |
||||||
* | ||||||
* @var array | ||||||
*/ | ||||||
protected $tag; | ||||||
|
||||||
/** | ||||||
* Returns an array of tokens this test wants to listen for. | ||||||
* | ||||||
* @return array | ||||||
*/ | ||||||
public function register() { | ||||||
return Tokens::$textStringTokens; | ||||||
} | ||||||
|
||||||
/** | ||||||
* Processes this test, when one of its tokens is encountered. | ||||||
* | ||||||
* @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the | ||||||
* token was found. | ||||||
* @param int $stackPtr The position of the current token | ||||||
* in the stack. | ||||||
* | ||||||
* @return void | ||||||
*/ | ||||||
public function process( File $phpcsFile, $stackPtr ) { | ||||||
|
||||||
$tokens = $phpcsFile->getTokens(); | ||||||
$content = $this->clean_str( $tokens[ $stackPtr ]['content'] ); | ||||||
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. See my comment about the |
||||||
$filename = $phpcsFile->getFileName(); | ||||||
|
||||||
// Set to false if it is the first time this sniff is run on a file. | ||||||
if ( ! isset( $this->tag[ $filename ] ) ) { | ||||||
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. 👍 on keeping track of which file you are in for matching the tags 👍 I think this can be simplified a little though for better performance. What about just having a string The only thing which would need to be verified in that case would be that this will work OK when running PHPCS with the 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 also wondering if |
||||||
$this->tag[ $filename ] = false; | ||||||
} | ||||||
|
||||||
// Skip on empty. | ||||||
if ( '' === $content ) { | ||||||
jrfnl marked this conversation as resolved.
Show resolved
Hide resolved
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. When not using the
Suggested change
|
||||||
return; | ||||||
} | ||||||
|
||||||
// Set tag class property. | ||||||
foreach ( $this->tagsConfig as $tag => $settings ) { | ||||||
|
||||||
// HTML case should be insensitive. | ||||||
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.
Suggested change
|
||||||
if ( false !== stripos( $content, '<' . $tag ) ) { | ||||||
$this->tag[ $filename ] = $this->tagsConfig[ $tag ]; | ||||||
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. Why not just keep track of the matched |
||||||
$this->tag[ $filename ]['tag'] = $tag; | ||||||
break; | ||||||
} | ||||||
} | ||||||
|
||||||
// Skip if not a tag. | ||||||
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.
Suggested change
|
||||||
if ( false === $this->tag[ $filename ] ) { | ||||||
return; | ||||||
} | ||||||
|
||||||
// Set vars used for reference. | ||||||
$tagName = $this->tag[ $filename ]['tag']; | ||||||
$tagFn = $this->tag[ $filename ]['function']; | ||||||
$tagAttr = $this->tag[ $filename ]['attribute']; | ||||||
$pascal = str_replace( ' ', '', ucwords( str_replace( '_', ' ', $tagFn ) ) ); | ||||||
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. This transformation is not needed unless an error will be thrown. I suggest moving it lower down. Also have a look at the WPCS |
||||||
$nextPtr = $stackPtr; | ||||||
$foundFunction = false; | ||||||
$foundAttribute = false; | ||||||
$foundEnd = false; | ||||||
|
||||||
do { | ||||||
$nextPtrContent = $this->clean_str( $tokens[ $nextPtr ]['content'] ); | ||||||
$nextPtrCode = $tokens[ $nextPtr ]['code']; | ||||||
|
||||||
// Check for attribute not allowed. | ||||||
if ( | ||||||
false === $foundAttribute && | ||||||
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. Just a small code-consistency note (which isn't checked for by the CS check yet): generally it is considered best practice to have the boolean operators at the start of the line containing the next condition as this improves readability. While not a formal requirement, as this is consistently done throughout the rest of the code-base, I'd like to suggest complying with that here and in other places in the sniff, as well. if (false === $foundAttribute
&& isset( Tokens::$textStringTokens[ $nextPtrCode ] )
&& false !== stripos( $nextPtrContent, $tagAttr . '=' )
) { |
||||||
isset( Tokens::$textStringTokens[ $nextPtrCode ] ) && | ||||||
false !== stripos( $nextPtrContent, $tagAttr . '=' ) | ||||||
) { | ||||||
$foundAttribute = true; | ||||||
} | ||||||
|
||||||
// Check for required function call. | ||||||
if ( | ||||||
false === $foundFunction && | ||||||
isset( Tokens::$functionNameTokens[ $nextPtrCode ] ) && | ||||||
false !== strpos( $nextPtrContent, $tagFn ) | ||||||
) { | ||||||
|
||||||
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. This code would also match |
||||||
// Check next non-whitespace token for opening parens. | ||||||
$next = $phpcsFile->findNext( Tokens::$emptyTokens, ( $nextPtr + 1 ), null, true ); | ||||||
|
||||||
if ( ! $next || ! isset( $tokens[ $next ] ) ) { | ||||||
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. This is sufficient.
Suggested change
|
||||||
break; // Nothing left. | ||||||
} | ||||||
|
||||||
// Verify function( $param = 'optional' ) type. | ||||||
if ( 'PHPCS_T_OPEN_PARENTHESIS' === $tokens[ $next ]['code'] ) { | ||||||
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.
Suggested change
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. Also: I'm not sure what you're trying to do here. |
||||||
|
||||||
// Skip over contents to closing parens in stack. | ||||||
if ( isset( $tokens[ $next ]['parenthesis_closer'] ) ) { | ||||||
$nextPtr = $tokens[ $next ]['parenthesis_closer']; | ||||||
$foundFunction = true; | ||||||
} | ||||||
} | ||||||
|
||||||
continue; | ||||||
} | ||||||
|
||||||
// Check for searched tag matched closing bracket. | ||||||
if ( | ||||||
isset( Tokens::$textStringTokens[ $nextPtrCode ] ) && | ||||||
'>' === substr( $nextPtrContent, -1 ) | ||||||
) { | ||||||
$this->tag[ $filename ] = false; | ||||||
$foundEnd = true; | ||||||
break; | ||||||
} | ||||||
|
||||||
// Increment stack to next non-whitespace token. | ||||||
$next = $phpcsFile->findNext( Tokens::$emptyTokens, ( $nextPtr + 1 ), null, true ); | ||||||
|
||||||
if ( ! $next || ! isset( $tokens[ $next ] ) ) { | ||||||
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. See above. |
||||||
break; // Short circuit loop as there's not anything left. | ||||||
} | ||||||
|
||||||
$nextPtr = $next; | ||||||
|
||||||
} while ( false === $foundEnd ); // Loop until matched closing bracket is found for searched tag. | ||||||
|
||||||
// Required function not found. | ||||||
if ( false === $foundFunction ) { | ||||||
$phpcsFile->addError( | ||||||
"Themes must call {$tagFn}() inside <{$tagName}> tags.", | ||||||
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. The PHPCS $phpcsFile->addError(
"Themes must call %s() inside <%s> tags.",
$stackPtr,
"RequiredFunction{$pascal}",
array(
$tagFn,
$tagName,
)
); |
||||||
$stackPtr, | ||||||
"RequiredFunction{$pascal}" | ||||||
); | ||||||
|
||||||
return; | ||||||
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. Having a return here effectively hides one error behind another which will make fixing things unwieldy, so it is better not to do this. |
||||||
} | ||||||
|
||||||
// Atrribute is not allowed. | ||||||
if ( true === $foundAttribute ) { | ||||||
$phpcsFile->addError( | ||||||
"Attribute '{$tagAttr}' is not allowed on <{$tagName}> tags. Themes must call {$tagFn}() instead.", | ||||||
$stackPtr, | ||||||
"DisallowedAttribute{$pascal}" | ||||||
); | ||||||
|
||||||
return; | ||||||
} | ||||||
} | ||||||
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. Please use |
||||||
|
||||||
/** | ||||||
* Cleans string for parsing. | ||||||
* | ||||||
* This cleans whitespace chars and single/double quotes | ||||||
* from string. Primary used to check T_CONSTANT_ENCAPSED_STRING | ||||||
* and T_DOUBLE_QUOTED_STRING for closing HTML brackets. This is | ||||||
* because < and > are valid attribute values, and a strpos wouldn't | ||||||
* be enough. | ||||||
* | ||||||
* Strips: | ||||||
* ' ' : Whitespace | ||||||
* '"' : double quote | ||||||
* ''' : single quote | ||||||
* '\t' : tab | ||||||
* '\n' : newline | ||||||
* '\r' : carriage return | ||||||
* '\0' : NUL-byte | ||||||
* '\x0B': vertical tab | ||||||
* | ||||||
* @param string $str String to clean. | ||||||
* | ||||||
* @return string Cleaned string. | ||||||
*/ | ||||||
private function clean_str( $str ) { | ||||||
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. 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. Funny you mentioned it - I initially made a Util class for this while testing it out, but after looking over WPThemeReview I didn't see one anywhere else, or a common area, so I just figured the "norm" was to keep functions self contained per class. I noticed some WPCS stuff that would be useful, but I haven't worked my way to that level lol. It looks like we are all trying to address the same issues revolving around
There were some other weird scenarios with doing things like mixing heredoc/functions(print, I'm not sure if this function will be useful anywhere else though, but I could see just the 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 would break the HTML. The 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. Re: I think this would be a good idea. If I remember correctly, the reasons why in WPCS all utility methods are in the abstract As far as I can see, as long as the file is within the standards directory, is namespaced based on the path and the file name doesn't end on A Maybe I suggest a Alternatively, for the 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. Sorry, but I'm going to be horrible and say this function is a bad idea. If a text string spans multiple lines, start/end string quotes may be on different lines. By "cleaning" those quotes of the strings in individual lines without matching them, you will be removing legitimate characters which are part of the string itself. If you want to clear the string quotes of PHP strings:
|
||||||
return trim( $str, " \"\'\t\n\r\0\x0B" ); | ||||||
} | ||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
<?php | ||
/** | ||
* Unit test for required functions in HTML tags. | ||
* | ||
* This tests language_attributes(), and body_class() requirements. | ||
*/ | ||
echo '<html>'; // Bad. | ||
echo '<body>'; // Bad. | ||
?> | ||
<html> <!-- Bad. --> | ||
<html <?php language_attributes(); ?> lang="en-US"> <!-- Bad. --> | ||
<?php | ||
echo '<html lang="en-US" '; // Bad. | ||
language_attributes(); | ||
echo '>'; | ||
?> | ||
<?php | ||
echo '<html '; // Ok. | ||
language_attributes(); | ||
echo '>'; | ||
?> | ||
<html <?php language_attributes(); ?> lang="en-US"> <!-- Bad. --> | ||
<?php echo '<body class="body-test" ', body_class(), '>'; // Bad. ?> | ||
<body> <!-- Bad. --> | ||
<?php echo '<body>'; // Bad. ?> | ||
<?php echo '<body class="body-test" ' . body_class() . '>'; // Bad. ?> | ||
<?php echo '<body class="body-test" ', body_class(), '>'; // Bad. ?> | ||
<html <?php language_attributes(); ?>> <!-- Ok. --> | ||
<html <?php language_attributes(); ?> class="html-class"> <!-- Ok. --> | ||
<?php echo '<body id="body-test" ' . body_class() . '>'; // Ok. ?> | ||
<?php echo '<body id="body-test" ', body_class(), '>'; // Ok. ?> | ||
<body <?php body_class(); ?>> <!-- Ok. --> | ||
|
||
<?php | ||
// Test common invalid heredoc style. | ||
$html = <<<EOT | ||
<!-- Bad. --> | ||
<body> | ||
... | ||
</body> | ||
EOT; | ||
|
||
// Test weird but valid heredoc style. | ||
$html = <<<EOT | ||
<!-- Ok. --> | ||
<body | ||
EOT; | ||
body_class(); | ||
$html = <<<EOT | ||
> | ||
... | ||
EOT; | ||
?> | ||
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.
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
<?php | ||
/** | ||
* Unit test class for WPThemeReview Coding Standard. | ||
* | ||
* @package WPTRT\WPThemeReview | ||
* @link https://github.com/WPTRT/WPThemeReview | ||
* @license https://opensource.org/licenses/MIT MIT | ||
*/ | ||
|
||
namespace WPThemeReview\Tests\Templates; | ||
|
||
use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest; | ||
|
||
/** | ||
* Unit test class for the RequiredFunction sniff. | ||
* | ||
* @since 0.1.0 | ||
*/ | ||
class RequiredFunctionUnitTest extends AbstractSniffUnitTest { | ||
|
||
/** | ||
* Returns the lines where errors should occur. | ||
* | ||
* @return array <int line number> => <int number of errors> | ||
*/ | ||
public function getErrorList() { | ||
return array( | ||
7 => 1, | ||
8 => 1, | ||
10 => 1, | ||
11 => 1, | ||
13 => 1, | ||
22 => 1, | ||
23 => 1, | ||
24 => 1, | ||
25 => 1, | ||
26 => 1, | ||
27 => 1, | ||
38 => 1, | ||
); | ||
} | ||
|
||
/** | ||
* Returns the lines where warnings should occur. | ||
* | ||
* @return array <int line number> => <int number of warnings> | ||
*/ | ||
public function getWarningList() { | ||
return 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.