Skip to content

Commit

Permalink
Tools/BrainMonkeyRaceCondition: decouple from WPCS sniff
Browse files Browse the repository at this point in the history
.... as extending the WPCS sniff is now no longer needed, as the utility functions we used from that sniff before, have all been replaced with utilities from PHPCSUtils.
  • Loading branch information
jrfnl committed Nov 4, 2023
1 parent 1b3d82e commit 6ea3d9b
Showing 1 changed file with 32 additions and 27 deletions.
59 changes: 32 additions & 27 deletions Yoast/Sniffs/Tools/BrainMonkeyRaceConditionSniff.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,25 @@

namespace YoastCS\Yoast\Sniffs\Tools;

use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Util\Tokens;
use PHPCSUtils\Tokens\Collections;
use PHPCSUtils\Utils\Conditions;
use PHPCSUtils\Utils\FunctionDeclarations;
use PHPCSUtils\Utils\PassedParameters;
use PHPCSUtils\Utils\Scopes;
use PHPCSUtils\Utils\TextStrings;
use WordPressCS\WordPress\Sniff;

/**
* Sniff to detect a particular nasty race condition which can occur in tests using the BrainMonkey utilities.
*
* @link https://github.com/Yoast/yoastcs/issues/264
*
* @since 2.3.0
* @since 3.0.0 This sniff no longer extends the WPCS abstract Sniff class.
*/
final class BrainMonkeyRaceConditionSniff extends Sniff {
final class BrainMonkeyRaceConditionSniff implements Sniff {

/**
* Returns an array of tokens this test wants to listen for.
Expand All @@ -30,63 +32,66 @@ public function register() {
}

/**
* Processes a sniff when one of its tokens is encountered.
* Processes this test, when one of its tokens is encountered.
*
* @param int $stackPtr The position of the current token in the stack.
* @param File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token in the stack passed in $tokens.
*
* @return void
*/
public function process_token( $stackPtr ) {
if ( \strtolower( $this->tokens[ $stackPtr ]['content'] ) !== 'expect' ) {
public function process( File $phpcsFile, $stackPtr ) {
$tokens = $phpcsFile->getTokens();

if ( \strtolower( $tokens[ $stackPtr ]['content'] ) !== 'expect' ) {
return;
}

$nextNonEmpty = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true );
if ( $nextNonEmpty === false || $this->tokens[ $nextNonEmpty ]['code'] !== \T_OPEN_PARENTHESIS ) {
$nextNonEmpty = $phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true );
if ( $nextNonEmpty === false || $tokens[ $nextNonEmpty ]['code'] !== \T_OPEN_PARENTHESIS ) {
// Definitely not a function call.
return;
}

$prevNonEmpty = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $stackPtr - 1 ), null, true );
$prevNonEmpty = $phpcsFile->findPrevious( Tokens::$emptyTokens, ( $stackPtr - 1 ), null, true );
if ( $prevNonEmpty === false
|| isset( Collections::objectOperators()[ $this->tokens[ $prevNonEmpty ]['code'] ] )
|| $this->tokens[ $prevNonEmpty ]['code'] === \T_FUNCTION
|| isset( Collections::objectOperators()[ $tokens[ $prevNonEmpty ]['code'] ] )
|| $tokens[ $prevNonEmpty ]['code'] === \T_FUNCTION
) {
// Method call or function declaration, not a function call.
return;
}

$functionToken = Conditions::getLastCondition( $this->phpcsFile, $stackPtr, [ \T_FUNCTION ] );
$functionToken = Conditions::getLastCondition( $phpcsFile, $stackPtr, [ \T_FUNCTION ] );
if ( $functionToken === false ) {
return;
}

if ( Scopes::isOOMethod( $this->phpcsFile, $functionToken ) === false ) {
if ( Scopes::isOOMethod( $phpcsFile, $functionToken ) === false ) {
return;
}

// Check that this is an expect() for one of the hook functions.
$param = PassedParameters::getParameter( $this->phpcsFile, $stackPtr, 1, 'function_name' );
$param = PassedParameters::getParameter( $phpcsFile, $stackPtr, 1, 'function_name' );
if ( empty( $param ) ) {
return;
}

$expected = Tokens::$emptyTokens;
$expected[] = \T_CONSTANT_ENCAPSED_STRING;

$hasUnexpected = $this->phpcsFile->findNext( $expected, $param['start'], ( $param['end'] + 1 ), true );
$hasUnexpected = $phpcsFile->findNext( $expected, $param['start'], ( $param['end'] + 1 ), true );
if ( $hasUnexpected !== false ) {
return;
}

$text = $this->phpcsFile->findNext( Tokens::$emptyTokens, $param['start'], ( $param['end'] + 1 ), true );
$textContent = TextStrings::stripQuotes( $this->tokens[ $text ]['content'] );
$text = $phpcsFile->findNext( Tokens::$emptyTokens, $param['start'], ( $param['end'] + 1 ), true );
$textContent = TextStrings::stripQuotes( $tokens[ $text ]['content'] );
if ( $textContent !== 'apply_filters' && $textContent !== 'do_action' ) {
return;
}

// Now walk the contents of the function declaration to see if we can find the other function call.
if ( isset( $this->tokens[ $functionToken ]['scope_opener'], $this->tokens[ $functionToken ]['scope_closer'] ) === false ) {
if ( isset( $tokens[ $functionToken ]['scope_opener'], $tokens[ $functionToken ]['scope_closer'] ) === false ) {
// We don't know the start or end of the function.
return;
}
Expand All @@ -96,33 +101,33 @@ public function process_token( $stackPtr ) {
$targetContent = 'expectapplied';
}

$start = $this->tokens[ $functionToken ]['scope_opener'];
$end = $this->tokens[ $functionToken ]['scope_closer'];
$start = $tokens[ $functionToken ]['scope_opener'];
$end = $tokens[ $functionToken ]['scope_closer'];

for ( $i = $start; $i < $end; $i++ ) {
if ( $this->tokens[ $i ]['code'] !== \T_STRING ) {
if ( $tokens[ $i ]['code'] !== \T_STRING ) {
continue;
}

if ( \strtolower( $this->tokens[ $i ]['content'] ) !== $targetContent ) {
if ( \strtolower( $tokens[ $i ]['content'] ) !== $targetContent ) {
continue;
}

// Make sure it is a function call.
$next = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $i + 1 ), null, true );
if ( $next === false || $this->tokens[ $next ]['code'] !== \T_OPEN_PARENTHESIS ) {
$next = $phpcsFile->findNext( Tokens::$emptyTokens, ( $i + 1 ), null, true );
if ( $next === false || $tokens[ $next ]['code'] !== \T_OPEN_PARENTHESIS ) {
continue;
}

// Okay, we have found the race condition. Throw error.
$message = 'The %s() test method contains both a call to Monkey\Functions\expect( %s ), as well as a call to %s(). This causes a race condition which will cause the tests to fail. Only use one of these in a test.';
$data = [
FunctionDeclarations::getName( $this->phpcsFile, $functionToken ),
$this->tokens[ $text ]['content'],
FunctionDeclarations::getName( $phpcsFile, $functionToken ),
$tokens[ $text ]['content'],
( $targetContent === 'expectdone' ) ? 'Monkey\Actions\expectDone' : 'Monkey\Filters\expectApplied',
];

$this->phpcsFile->addError( $message, $functionToken, 'Found', $data );
$phpcsFile->addError( $message, $functionToken, 'Found', $data );
break;
}
}
Expand Down

0 comments on commit 6ea3d9b

Please sign in to comment.