Skip to content
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

3.0: Move "is unit test" utility functions to dedicated trait #1960

Merged
merged 8 commits into from
Feb 20, 2021
177 changes: 177 additions & 0 deletions WordPress/Helpers/IsUnitTestTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/

namespace WordPressCS\WordPress\Helpers;

use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Util\Tokens;
use PHPCSUtils\Utils\Namespaces;
use PHPCSUtils\Utils\ObjectDeclarations;
use WordPressCS\WordPress\Sniff as WPCS_Sniff;

/**
* Helper utilities for sniffs which need to take into account whether the
* code under examination is unit test code or not.
*
* Usage instructions:
* - Add appropriate `use` statement(s) to the file/class which intends to use this functionality.
* - Now the sniff will automatically support the public `custom_test_classes` property which
* users can set in their custom ruleset. Do not add the property to the sniff!
* - The sniff can call the methods in this trait to verify if certain code was found within
* a test method or is a test class and will take the custom property into account.
*
* @package WPCS\WordPressCodingStandards
* @since 3.0.0 The properties and method in this trait were previously contained in the
* `WordPressCS\WordPress\Sniff` class and have been moved here.
*/
trait IsUnitTestTrait {

/**
* Custom list of classes which test classes can extend.
*
* This property allows end-users to add to the build-in `$known_test_classes`
* via their custom PHPCS ruleset.
* This property will need to be set for each sniff which uses this trait.
*
* Currently this property is used by the `WordPress.WP.GlobalVariablesOverride`,
* `WordPress.NamingConventions.PrefixAllGlobals` and the `WordPress.Files.Filename` sniffs.
*
* Example usage:
* ```xml
* <rule ref="WordPress.[Subset].[Sniffname]">
* <properties>
* <property name="custom_test_classes" type="array">
* <element value="My_Plugin_First_Test_Class"/>
* <element value="My_Plugin_Second_Test_Class"/>
* </property>
* </properties>
* </rule>
* ```
*
* Note: it is strongly _recommended_ to exclude your test directories for
* select error codes of those particular sniffs instead of relying on this
* property/trait.
*
* @since 0.11.0
* @since 3.0.0 Moved from the Sniff class to this dedicated Trait.
* Renamed from `$custom_test_class_whitelist` to `$custom_test_classes`.
*
* @var string|string[]
*/
public $custom_test_classes = array();

/**
* List of PHPUnit and WP native classes which test classes can extend.
*
* {internal These are the test cases provided in the `/tests/phpunit/includes/`
* directory of WP Core.}
*
* @since 0.11.0
* @since 3.0.0 Moved from the Sniff class to this dedicated Trait.
* Renamed from `$test_class_whitelist` to `$known_test_classes`.
*
* @var string[]
*/
protected $known_test_classes = array(
'WP_UnitTestCase_Base' => true,
'WP_UnitTestCase' => true,
'WP_Ajax_UnitTestCase' => true,
'Block_Supported_Styles_Test' => true,
'WP_Canonical_UnitTestCase' => true,
'WP_Test_REST_TestCase' => true,
'WP_Test_REST_Controller_Testcase' => true,
'WP_Test_REST_Post_Type_Controller_Testcase' => true,
'WP_Test_XML_TestCase' => true,
'WP_XMLRPC_UnitTestCase' => true,
'PHPUnit_Framework_TestCase' => true,
'PHPUnit\Framework\TestCase' => true,
// PHPUnit native TestCase class when imported via use statement.
'TestCase' => true,
);

/**
* Check if a class token is part of a unit test suite.
*
* Unit test classes are identified as such:
* - Class which either extends one of the known test cases, such as `WP_UnitTestCase`
* or `PHPUnit_Framework_TestCase` or extends a custom unit test class as listed in the
* `custom_test_classes` property.
*
* @since 0.12.0 Split off from the `is_token_in_test_method()` method.
* @since 1.0.0 Improved recognition of namespaced class names.
* @since 3.0.0 Moved from the Sniff class to this dedicated Trait.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the token to be examined.
* This should be a class, anonymous class or trait token.
*
* @return bool True if the class is a unit test class, false otherwise.
*/
protected function is_test_class( File $phpcsFile, $stackPtr ) {

$tokens = $phpcsFile->getTokens();

if ( isset( $tokens[ $stackPtr ], Tokens::$ooScopeTokens[ $tokens[ $stackPtr ]['code'] ] ) === false ) {
return false;
}

// Add any potentially extra custom test classes to the known test classes list.
$known_test_classes = WPCS_Sniff::merge_custom_array(
$this->custom_test_classes,
$this->known_test_classes
);

/*
* Show some tolerance for user input.
* The custom test class names should be passed as FQN without a prefixing `\`.
*/
foreach ( $known_test_classes as $k => $v ) {
$known_test_classes[ $k ] = ltrim( $v, '\\' );
}

// Is the class/trait one of the whitelisted test classes ?
$namespace = Namespaces::determineNamespace( $phpcsFile, $stackPtr );
$className = ObjectDeclarations::getName( $phpcsFile, $stackPtr );
if ( '' !== $namespace ) {
if ( isset( $known_test_classes[ $namespace . '\\' . $className ] ) ) {
return true;
}
} elseif ( isset( $known_test_classes[ $className ] ) ) {
return true;
}

// Does the class/trait extend one of the whitelisted test classes ?
$extendedClassName = ObjectDeclarations::findExtendedClassName( $phpcsFile, $stackPtr );
if ( false === $extendedClassName ) {
return false;
}

if ( '\\' === $extendedClassName[0] ) {
if ( isset( $known_test_classes[ substr( $extendedClassName, 1 ) ] ) ) {
return true;
}
} elseif ( '' !== $namespace ) {
if ( isset( $known_test_classes[ $namespace . '\\' . $extendedClassName ] ) ) {
return true;
}
} elseif ( isset( $known_test_classes[ $extendedClassName ] ) ) {
return true;
}

/*
* Not examining imported classes via `use` statements as with the variety of syntaxes,
* this would get very complicated.
* After all, users can add an `<exclude-pattern>` for a particular sniff to their
* custom ruleset to selectively exclude the test directory.
*/

return false;
}

}
162 changes: 0 additions & 162 deletions WordPress/Sniff.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Util\Tokens;
use PHPCSUtils\Utils\Lists;
use PHPCSUtils\Utils\Namespaces;
use PHPCSUtils\Utils\PassedParameters;
use PHPCSUtils\Utils\Scopes;
use PHPCSUtils\Utils\TextStrings;
Expand Down Expand Up @@ -48,31 +47,6 @@ abstract class Sniff implements PHPCS_Sniff {
*/
const REGEX_COMPLEX_VARS = '`(?:(\{)?(?<!\\\\)\$)?(\{)?(?<!\\\\)\$(\{)?(?P<varname>[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)(?:->\$?(?P>varname)|\[[^\]]+\]|::\$?(?P>varname)|\([^\)]*\))*(?(3)\}|)(?(2)\}|)(?(1)\}|)`';

/**
* Custom list of classes which test classes can extend.
*
* This property allows end-users to add to the $test_class_whitelist via their ruleset.
* This property will need to be set for each sniff which uses the
* `is_test_class()` method.
* Currently the method is used by the `WordPress.WP.GlobalVariablesOverride`,
* `WordPress.NamingConventions.PrefixAllGlobals` and the `WordPress.Files.Filename` sniffs.
*
* Example usage:
* <rule ref="WordPress.[Subset].[Sniffname]">
* <properties>
* <property name="custom_test_class_whitelist" type="array">
* <element value="My_Plugin_First_Test_Class"/>
* <element value="My_Plugin_Second_Test_Class"/>
* </property>
* </properties>
* </rule>
*
* @since 0.11.0
*
* @var string|string[]
*/
public $custom_test_class_whitelist = array();

/**
* List of the functions which verify nonces.
*
Expand Down Expand Up @@ -817,28 +791,6 @@ abstract class Sniff implements PHPCS_Sniff {
'$_SERVER',
);

/**
* Whitelist of classes which test classes can extend.
*
* @since 0.11.0
*
* @var string[]
*/
protected $test_class_whitelist = array(
'WP_UnitTestCase_Base' => true,
'WP_UnitTestCase' => true,
'WP_Ajax_UnitTestCase' => true,
'WP_Canonical_UnitTestCase' => true,
'WP_Test_REST_TestCase' => true,
'WP_Test_REST_Controller_Testcase' => true,
'WP_Test_REST_Post_Type_Controller_Testcase' => true,
'WP_XMLRPC_UnitTestCase' => true,
'PHPUnit_Framework_TestCase' => true,
'PHPUnit\Framework\TestCase' => true,
// PHPUnit native TestCase class when imported via use statement.
'TestCase' => true,
);

/**
* The current file being sniffed.
*
Expand Down Expand Up @@ -1080,120 +1032,6 @@ protected function get_last_ptr_on_line( $stackPtr ) {
return $lastPtr;
}

/**
* Check if a token is used within a unit test.
*
* Unit test methods are identified as such:
* - Method is within a known unit test class;
* - or Method is within a class/trait which extends a known unit test class.
*
* @since 0.11.0
* @since 1.1.0 Supports anonymous test classes and improved handling of nested scopes.
*
* @param int $stackPtr The position of the token to be examined.
*
* @return bool True if the token is within a unit test, false otherwise.
*/
protected function is_token_in_test_method( $stackPtr ) {
// Is the token inside of a function definition ?
$functionToken = $this->phpcsFile->getCondition( $stackPtr, \T_FUNCTION );
if ( false === $functionToken ) {
// No conditions or no function condition.
return false;
}

$conditions = $this->tokens[ $stackPtr ]['conditions'];
foreach ( $conditions as $token => $condition ) {
if ( $token === $functionToken ) {
// Only examine the conditions the function is nested in, not those nested within the function.
break;
}

if ( isset( Tokens::$ooScopeTokens[ $condition ] ) ) {
$is_test_class = $this->is_test_class( $token );
if ( true === $is_test_class ) {
return true;
}
}
}

return false;
}

/**
* Check if a class token is part of a unit test suite.
*
* Unit test classes are identified as such:
* - Class which either extends WP_UnitTestCase or PHPUnit_Framework_TestCase
* or a custom whitelisted unit test class.
*
* @since 0.12.0 Split off from the `is_token_in_test_method()` method.
* @since 1.0.0 Improved recognition of namespaced class names.
*
* @param int $stackPtr The position of the token to be examined.
* This should be a class, anonymous class or trait token.
*
* @return bool True if the class is a unit test class, false otherwise.
*/
protected function is_test_class( $stackPtr ) {

if ( isset( $this->tokens[ $stackPtr ], Tokens::$ooScopeTokens[ $this->tokens[ $stackPtr ]['code'] ] ) === false ) {
return false;
}

// Add any potentially whitelisted custom test classes to the whitelist.
$whitelist = $this->merge_custom_array(
$this->custom_test_class_whitelist,
$this->test_class_whitelist
);

/*
* Show some tolerance for user input.
* The custom test class names should be passed as FQN without a prefixing `\`.
*/
foreach ( $whitelist as $k => $v ) {
$whitelist[ $k ] = ltrim( $v, '\\' );
}

// Is the class/trait one of the whitelisted test classes ?
$namespace = Namespaces::determineNamespace( $this->phpcsFile, $stackPtr );
$className = $this->phpcsFile->getDeclarationName( $stackPtr );
if ( '' !== $namespace ) {
if ( isset( $whitelist[ $namespace . '\\' . $className ] ) ) {
return true;
}
} elseif ( isset( $whitelist[ $className ] ) ) {
return true;
}

// Does the class/trait extend one of the whitelisted test classes ?
$extendedClassName = $this->phpcsFile->findExtendedClassName( $stackPtr );
if ( false === $extendedClassName ) {
return false;
}

if ( '\\' === $extendedClassName[0] ) {
if ( isset( $whitelist[ substr( $extendedClassName, 1 ) ] ) ) {
return true;
}
} elseif ( '' !== $namespace ) {
if ( isset( $whitelist[ $namespace . '\\' . $extendedClassName ] ) ) {
return true;
}
} elseif ( isset( $whitelist[ $extendedClassName ] ) ) {
return true;
}

/*
* Not examining imported classes via `use` statements as with the variety of syntaxes,
* this would get very complicated.
* After all, users can add an `<exclude-pattern>` for a particular sniff to their
* custom ruleset to selectively exclude the test directory.
*/

return false;
}

/**
* Check if this variable is being assigned a value.
*
Expand Down
7 changes: 5 additions & 2 deletions WordPress/Sniffs/Files/FileNameSniff.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

use PHPCSUtils\Utils\TextStrings;
use WordPressCS\WordPress\Sniff;
use WordPressCS\WordPress\Helpers\IsUnitTestTrait;

/**
* Ensures filenames do not contain underscores.
Expand All @@ -29,10 +30,12 @@
* @since 0.12.0 Now extends the WordPressCS native `Sniff` class.
* @since 0.13.0 Class name changed: this class is now namespaced.
*
* @uses \WordPressCS\WordPress\Sniff::$custom_test_class_whitelist
* @uses \WordPressCS\WordPress\Helpers\IsUnitTestTrait::$custom_test_classes
*/
class FileNameSniff extends Sniff {

use IsUnitTestTrait;

/**
* Regex for the theme specific exceptions.
*
Expand Down Expand Up @@ -192,7 +195,7 @@ public function process_token( $stackPtr ) {
*/
if ( true === $this->strict_class_file_names ) {
$has_class = $this->phpcsFile->findNext( \T_CLASS, $stackPtr );
if ( false !== $has_class && false === $this->is_test_class( $has_class ) ) {
if ( false !== $has_class && false === $this->is_test_class( $this->phpcsFile, $has_class ) ) {
$class_name = $this->phpcsFile->getDeclarationName( $has_class );
$expected = 'class-' . strtolower( str_replace( '_', '-', $class_name ) );

Expand Down
Loading