diff --git a/WordPressVIPMinimum/Sniffs/Filters/AlwaysReturnSniff.php b/WordPressVIPMinimum/Sniffs/Filters/AlwaysReturnSniff.php new file mode 100644 index 00000000..95e6fe19 --- /dev/null +++ b/WordPressVIPMinimum/Sniffs/Filters/AlwaysReturnSniff.php @@ -0,0 +1,301 @@ +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; + } +} diff --git a/WordPressVIPMinimum/Tests/Filters/AlwaysReturnUnitTest.inc b/WordPressVIPMinimum/Tests/Filters/AlwaysReturnUnitTest.inc new file mode 100644 index 00000000..3fb5ce08 --- /dev/null +++ b/WordPressVIPMinimum/Tests/Filters/AlwaysReturnUnitTest.inc @@ -0,0 +1,103 @@ + => + */ + public function getErrorList() { + return array(); + } + + /** + * Returns the lines where warnings should occur. + * + * @return array => + */ + public function getWarningList() { + return array( + 15 => 1, + 49 => 1, + 88 => 1, + 95 => 1, + ); + } + +} // End class.