WordPress-Coding-Standards/WordPress/AbstractClassRestrictionsSniff.php
jrfnl f131da83f8
AbstractClassRestrictionsSniff: fix inconsequential typo
The sniff looks for whitespace or a close curly at the end of a class declaration statement, but those don't end with a close curly, but with an open curly.

As it would be rare for anyone not to have whitespace before the open curly (which is also enforced by WPCS), this bug will probably never have had any consequences in real life, as the `findNext()` would stop at the whitespace anyway.

Having said that, I see quite a lot more wrong with the code in this abstract, but will leave that for the future abstract in PHPCSUtils to fix as spending lots of time on it here is not worth our time.
2025-12-09 14:41:23 +01:00

268 lines
8.5 KiB
PHP

<?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;
use PHP_CodeSniffer\Util\Tokens;
use PHPCSUtils\Tokens\Collections;
use PHPCSUtils\Utils\GetTokensAsString;
use PHPCSUtils\Utils\Namespaces;
use WordPressCS\WordPress\AbstractFunctionRestrictionsSniff;
use WordPressCS\WordPress\Helpers\RulesetPropertyHelper;
/**
* Restricts usage of some classes.
*
* @since 0.10.0
*/
abstract class AbstractClassRestrictionsSniff extends AbstractFunctionRestrictionsSniff {
/**
* Regex pattern with placeholder for the class names.
*
* @var string
*/
protected $regex_pattern = '`^\\\\(?:%s)$`i';
/**
* Temporary storage for retrieved class name.
*
* @var string
*/
protected $classname;
/**
* Groups of classes to restrict.
*
* This method should be overridden in extending classes.
*
* Example: groups => array(
* 'lambda' => array(
* 'type' => 'error' | 'warning',
* 'message' => 'Avoid direct calls to the database.',
* 'classes' => array( 'PDO', '\Namespace\Classname' ),
* )
* )
*
* You can use * wildcards to target a group of (namespaced) classes.
* Aliased namespaces (use ..) are currently not supported.
*
* Documented here for clarity. Not (re)defined as it is already defined in the parent class.
*
* @return array
*
abstract public function getGroups();
*/
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register() {
// Prepare the function group regular expressions only once.
if ( false === $this->setup_groups( 'classes' ) ) {
return array();
}
return array(
\T_DOUBLE_COLON,
\T_NEW,
\T_EXTENDS,
\T_IMPLEMENTS,
);
}
/**
* Processes this test, when one of its tokens is encountered.
*
* {@internal Unlike in the `AbstractFunctionRestrictionsSniff`,
* we can't do a preliminary check on classes as at this point
* we don't know the class name yet.}}
*
* @param int $stackPtr The position of the current token in the stack.
*
* @return int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
public function process_token( $stackPtr ) {
// Reset the temporary storage before processing the token.
unset( $this->classname );
$this->excluded_groups = RulesetPropertyHelper::merge_custom_array( $this->exclude );
if ( array_diff_key( $this->groups, $this->excluded_groups ) === array() ) {
// All groups have been excluded.
// Don't remove the listener as the exclude property can be changed inline.
return;
}
if ( true === $this->is_targetted_token( $stackPtr ) ) {
return $this->check_for_matches( $stackPtr );
}
}
/**
* Determine if we have a valid classname for the target token.
*
* @since 0.11.0 This logic was originally contained in the `process()` method.
*
* @param int $stackPtr The position of the current token in the stack.
*
* @return bool
*/
public function is_targetted_token( $stackPtr ) {
$token = $this->tokens[ $stackPtr ];
$classname = '';
if ( \in_array( $token['code'], array( \T_NEW, \T_EXTENDS, \T_IMPLEMENTS ), true ) ) {
if ( \T_NEW === $token['code'] ) {
$nextNonEmpty = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true );
if ( false === $nextNonEmpty
|| \in_array( $this->tokens[ $nextNonEmpty ]['code'], array( \T_READONLY, \T_ANON_CLASS, \T_ATTRIBUTE ), true )
) {
// Live coding or anonymous class. Bow out.
return false;
}
$nameEnd = ( $this->phpcsFile->findNext( array( \T_OPEN_PARENTHESIS, \T_WHITESPACE, \T_SEMICOLON, \T_CLOSE_PARENTHESIS, \T_CLOSE_TAG ), ( $stackPtr + 2 ) ) - 1 );
} else {
$nameEnd = ( $this->phpcsFile->findNext( array( \T_OPEN_CURLY_BRACKET, \T_WHITESPACE ), ( $stackPtr + 2 ) ) - 1 );
}
if ( isset( $this->tokens[ $stackPtr + 2 ] ) && false !== $nameEnd ) {
$classname = GetTokensAsString::noEmpties( $this->phpcsFile, ( $stackPtr + 2 ), $nameEnd );
$classname = $this->get_namespaced_classname( $classname, ( $stackPtr - 1 ) );
}
}
if ( \T_DOUBLE_COLON === $token['code'] ) {
$nameEnd = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $stackPtr - 1 ), null, true );
if ( \T_STRING !== $this->tokens[ $nameEnd ]['code'] ) {
// Hierarchy keyword or object stored in variable.
return false;
}
$nameStart = ( $this->phpcsFile->findPrevious( Collections::namespacedNameTokens(), ( $nameEnd - 1 ), null, true ) + 1 );
$classname = GetTokensAsString::noEmpties( $this->phpcsFile, $nameStart, $nameEnd );
$classname = $this->get_namespaced_classname( $classname, ( $nameStart - 1 ) );
}
// Stop if we couldn't determine a classname.
if ( empty( $classname ) ) {
return false;
}
// Nothing to do if one of the hierarchy keywords - 'parent', 'self' or 'static' - is used.
if ( \in_array( strtolower( $classname ), array( '\parent', '\self', '\static' ), true ) ) {
return false;
}
$this->classname = $classname;
return true;
}
/**
* Verify if the current token is one of the targeted classes.
*
* @since 0.11.0 Split out from the `process()` method.
*
* @param int $stackPtr The position of the current token in the stack.
*
* @return int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
public function check_for_matches( $stackPtr ) {
$skip_to = array();
foreach ( $this->groups as $groupName => $group ) {
if ( isset( $this->excluded_groups[ $groupName ] ) ) {
continue;
}
if ( preg_match( $group['regex'], $this->classname ) === 1 ) {
$skip_to[] = $this->process_matched_token( $stackPtr, $groupName, $this->classname );
}
}
if ( empty( $skip_to ) || min( $skip_to ) === 0 ) {
return;
}
return min( $skip_to );
}
/**
* Process a matched token.
*
* @since 0.11.0 Split out from the `process()` method.
*
* @param int $stackPtr The position of the current token in the stack.
* @param string $group_name The name of the group which was matched.
* @param string $matched_content The token content (function name) which was matched
* in it original case.
*
* @return int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*
* @phpcs:disable Generic.CodeAnalysis.UselessOverridingMethod.Found
*/
public function process_matched_token( $stackPtr, $group_name, $matched_content ) {
parent::process_matched_token( $stackPtr, $group_name, $matched_content );
}
// phpcs:enable
/**
* Prepare the class name for use in a regular expression.
*
* The getGroups() method allows for providing class names with a wildcard * to target
* a group of classes within a namespace. It also allows for providing class names as
* 'ordinary' names or prefixed with one or more namespaces.
* This prepare routine takes that into account while still safely escaping the
* class name for use in a regular expression.
*
* @param string $classname Class name, potentially prefixed with namespaces.
* @return string Regex escaped class name.
*/
protected function prepare_name_for_regex( $classname ) {
$classname = trim( $classname, '\\' ); // Make sure all classnames have a \ prefix, but only one.
return parent::prepare_name_for_regex( $classname );
}
/**
* See if the classname was found in a namespaced file and if so, add the namespace to the classname.
*
* @param string $classname The full classname as found.
* @param int $search_from The token position to search up from.
* @return string Classname, potentially prefixed with the namespace.
*/
protected function get_namespaced_classname( $classname, $search_from ) {
// Don't do anything if this is already a fully qualified classname.
if ( empty( $classname ) || '\\' === $classname[0] ) {
return $classname;
}
// Remove the namespace keyword if used.
if ( 0 === stripos( $classname, 'namespace\\' ) ) {
$classname = substr( $classname, 10 );
}
$namespace = Namespaces::determineNamespace( $this->phpcsFile, $search_from );
if ( '' === $namespace ) {
// No namespace keyword found at all, so global namespace.
$classname = '\\' . $classname;
} else {
$classname = '\\' . $namespace . '\\' . $classname;
}
return $classname;
}
}