Skip to content

Commit

Permalink
Merge pull request #177 from Automattic/add-filters-always-return-sniff
Browse files Browse the repository at this point in the history
Add filters always return sniff
  • Loading branch information
sboisvert authored Aug 9, 2018
2 parents 110ae5f + 2f682b8 commit 917c5a4
Show file tree
Hide file tree
Showing 3 changed files with 445 additions and 0 deletions.
301 changes: 301 additions & 0 deletions WordPressVIPMinimum/Sniffs/Filters/AlwaysReturnSniff.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
<?php
/**
* WordPressVIPMinimum Coding Standard.
*
* @package VIPCS\WordPressVIPMinimum
*/

namespace WordPressVIPMinimum\Sniffs\Filters;

use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Tokens as Tokens;

/**
* This sniff validates that filters always return a value
*
* @package VIPCS\WordPressVIPMinimum
*/
class AlwaysReturnSniff implements \PHP_CodeSniffer_Sniff {

/**
* The tokens of the phpcsFile.
*
* @var array
*/
private $tokens;

/**
* The phpcsFile.
*
* @var phpcsFile
*/
private $phpcsFile;

/**
* Filter name pointer.
*
* @var int
*/
private $filterNamePtr;

/**
* Returns the token types that this sniff is interested in.
*
* @return array(int)
*/
public function register() {
return Tokens::$functionNameTokens;

}//end register()

/**
* Processes the tokens that this sniff is interested in.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file where the token was found.
* @param int $stackPtr The position in the stack where
* the token was found.
*
* @return void
*/
public function process( File $phpcsFile, $stackPtr ) {

$this->tokens = $phpcsFile->getTokens();

$this->phpcsFile = $phpcsFile;

$functionName = $this->tokens[ $stackPtr ]['content'];

if ( 'add_filter' !== $functionName ) {
return;
}

$this->filterNamePtr = $this->phpcsFile->findNext(
array_merge( Tokens::$emptyTokens, array( T_OPEN_PARENTHESIS ) ), // types.
$stackPtr + 1, // start.
null, // end.
true, // exclude.
null, // value.
true // local.
);

if ( ! $this->filterNamePtr ) {
// Something is wrong.
return;
}

$callbackPtr = $this->phpcsFile->findNext(
array_merge( Tokens::$emptyTokens, array( T_COMMA ) ), // types.
$this->filterNamePtr + 1, // start.
null, // end.
true, // exclude.
null, // value.
true // local.
);

if ( ! $callbackPtr ) {
// Something is wrong.
return;
}

if ( 'PHPCS_T_CLOSURE' === $this->tokens[ $callbackPtr ]['code'] ) {
$this->processFunctionBody( $callbackPtr );
} elseif ( 'T_ARRAY' === $this->tokens[ $callbackPtr ]['type'] ) {
$this->processArray( $callbackPtr );
} elseif ( true === in_array( $this->tokens[ $callbackPtr ]['code'], Tokens::$stringTokens, true ) ) {
$this->processString( $callbackPtr );
}

}

/**
* Process array.
*
* @param int $stackPtr The position in the stack where the token was found.
*/
private function processArray( $stackPtr ) {

$previous = $this->phpcsFile->findPrevious(
Tokens::$emptyTokens, // types.
$this->tokens[ $stackPtr ]['parenthesis_closer'] - 1, // start.
null, // end.
true, // exclude.
null, // value.
false // local.
);

if ( true === in_array( T_CLASS, $this->tokens[ $stackPtr ]['conditions'], true ) ) {
$classPtr = array_search( T_CLASS, $this->tokens[ $stackPtr ]['conditions'], true );
if ( $classPtr ) {
$classToken = $this->tokens[ $classPtr ];
$this->processString( $previous, $classToken['scope_opener'], $classToken['scope_closer'] );
return;
}
}

$this->processString( $previous );

}

/**
* Process string.
*
* @param int $stackPtr The position in the stack where the token was found.
* @param int $start The start of the token.
* @param int $end The end of the token.
*/
private function processString( $stackPtr, $start = 0, $end = null ) {

$callbackFunctionName = substr( $this->tokens[ $stackPtr ]['content'], 1, -1 );

$callbackFunctionPtr = $this->phpcsFile->findNext(
Tokens::$functionNameTokens, // types.
$start, // start.
$end, // end.
false, // exclude.
$callbackFunctionName, // value.
false // local.
);

if ( ! $callbackFunctionPtr ) {
// We were not able to find the function callback in the file.
return;
}

$this->processFunction( $callbackFunctionPtr, $start, $end );

}

/**
* Process function.
*
* @param int $stackPtr The position in the stack where the token was found.
* @param int $start The start of the token.
* @param int $end The end of the token.
*/
private function processFunction( $stackPtr, $start = 0, $end = null ) {

$functionName = $this->tokens[ $stackPtr ]['content'];

$offset = $start;
while ( $functionStackPtr = $this->phpcsFile->findNext( array( T_FUNCTION ), $offset, $end, false, null, false ) ) {
$functionNamePtr = $this->phpcsFile->findNext( Tokens::$emptyTokens, $functionStackPtr + 1, null, true, null, true );
if ( T_STRING === $this->tokens[ $functionNamePtr ]['code'] ) {
if ( $this->tokens[ $functionNamePtr ]['content'] === $functionName ) {
$this->processFunctionBody( $functionStackPtr );
return;
}
}
$offset = $functionStackPtr + 1;
}
}

/**
* Process function's body
*
* @param int $stackPtr The position in the stack where the token was found.
*/
private function processFunctionBody( $stackPtr ) {

$filterName = $this->tokens[ $this->filterNamePtr ]['content'];

$functionBodyScopeStart = $this->tokens[ $stackPtr ]['scope_opener'];
$functionBodyScopeEnd = $this->tokens[ $stackPtr ]['scope_closer'];

$returnTokenPtr = $this->phpcsFile->findNext(
array( T_RETURN ), // types.
( $functionBodyScopeStart + 1 ), // start.
$functionBodyScopeEnd, // end.
false, // exclude.
null, // value.
false // local.
);

$insideIfConditionalReturn = 0;
$outsideConditionalReturn = 0;

while ( $returnTokenPtr ) {
if ( $this->isInsideIfConditonal( $returnTokenPtr ) ) {
$insideIfConditionalReturn++;
} else {
$outsideConditionalReturn++;
}
if ( $this->isReturningVoid( $returnTokenPtr ) ) {
$this->phpcsFile->AddWarning( sprintf( 'Please, make sure that a callback to `%s` filter is returnin void intentionally.', $filterName ), $functionBodyScopeStart, 'voidReturn' );
}
$returnTokenPtr = $this->phpcsFile->findNext(
array( T_RETURN ), // types.
( $returnTokenPtr + 1 ), // start.
$functionBodyScopeEnd, // end.
false, // exclude.
null, // value.
false // local.
);
}

if ( 0 < $insideIfConditionalReturn && 0 === $outsideConditionalReturn ) {
$this->phpcsFile->AddWarning( sprintf( 'Please, make sure that a callback to `%s` filter is always returning some value.', $filterName ), $functionBodyScopeStart, 'missingReturnStatement' );
}

}

/**
* Is the current token inside a conditional?
*
* @param int $stackPtr The position in the stack where the token was found.
*
* @return bool
*/
private function isInsideIfConditonal( $stackPtr ) {

// This check helps us in situations a class or a function is wrapped
// inside a conditional as a whole. Eg.: inside `class_exists`.
if ( T_FUNCTION === end( $this->tokens[ $stackPtr ]['conditions'] ) ) {
return false;
}

// Similar case may be a conditional closure.
if ( 'PHPCS_T_CLOSURE' === end( $this->tokens[ $stackPtr ]['conditions'] ) ) {
return false;
}

// Loop over the array of conditions and look for an IF.
reset( $this->tokens[ $stackPtr ]['conditions'] );

if ( true === array_key_exists( 'conditions', $this->tokens[ $stackPtr ] )
&& true === is_array( $this->tokens[ $stackPtr ]['conditions'] )
&& false === empty( $this->tokens[ $stackPtr ]['conditions'] )
) {
foreach ( $this->tokens[ $stackPtr ]['conditions'] as $tokenPtr => $tokenCode ) {
if ( T_IF === $this->tokens[ $stackPtr ]['conditions'][ $tokenPtr ] ) {
return true;
}
}
}
return false;
}

/**
* Is the token returning void
*
* @param int $stackPtr The position in the stack where the token was found.
*
* @return bool
**/
private function isReturningVoid( $stackPtr ) {

$nextToReturnTokenPtr = $this->phpcsFile->findNext(
array( Tokens::$emptyTokens ), // types.
( $stackPtr + 1 ), // start.
null, // end.
true, // exclude.
null, // value.
false // local.
);

if ( T_SEMICOLON === $this->tokens[ $nextToReturnTokenPtr ]['code'] ) {
return true;
}

return false;
}
}
Loading

0 comments on commit 917c5a4

Please sign in to comment.