Skip to content

Commit

Permalink
Merge pull request #334 from Yoast/JRF/yoastcs-objectnamedepth-variou…
Browse files Browse the repository at this point in the history
…s-improvements

NamingConventions/ObjectNameDepth: bug fix, support modern PHP and other improvements
  • Loading branch information
jrfnl authored Nov 4, 2023
2 parents 80457d5 + dc6695c commit bc72022
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 54 deletions.
4 changes: 2 additions & 2 deletions Yoast/Docs/NamingConventions/ObjectNameDepthStandard.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
>
<standard>
<![CDATA[
The name of objects - classes, interfaces, traits - declared within a namespace should consist of a maximum of three words.
The name of OO structures - classes, interfaces, traits, enums - declared within a namespace should consist of a maximum of three words.
A partial exception is made for test, mock and double classes. These can have a `_Test`, `_Mock` or `_Double` class name suffix, which won't be counted.
A partial exception is made for test, mock and double classes. These can have a `_Test`, `_TestCase`, `_Mock` or `_Double` class name suffix, which won't be counted.
Note: the maximum (error) and the recommended (warning) maximum length are configurable.
]]>
Expand Down
103 changes: 54 additions & 49 deletions Yoast/Sniffs/NamingConventions/ObjectNameDepthSniff.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,36 @@

namespace YoastCS\Yoast\Sniffs\NamingConventions;

use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Util\Tokens;
use PHPCSUtils\Tokens\Collections;
use PHPCSUtils\Utils\Namespaces;
use PHPCSUtils\Utils\ObjectDeclarations;
use WordPressCS\WordPress\Helpers\IsUnitTestTrait;
use WordPressCS\WordPress\Helpers\SnakeCaseHelper;
use WordPressCS\WordPress\Sniff as WPCS_Sniff;

/**
* Check the number of words in object names declared within a namespace.
*
* @since 2.0.0
*
* @uses \WordPressCS\WordPress\Helpers\IsUnitTestTrait::$custom_test_classes
* @since 3.0.0 This sniff no longer extends the WPCS abstract Sniff class.
*/
final class ObjectNameDepthSniff extends WPCS_Sniff {

use IsUnitTestTrait;
final class ObjectNameDepthSniff implements Sniff {

/**
* Suffixes commonly used for classes in the test suites.
*
* The key is the suffix. The value indicates whether this suffix is
* The key is the suffix in lowercase. The value indicates whether this suffix is
* only allowed when the class extends a known test class.
* The value is currently unused.
*
* @var array<string, bool>
*/
private const TEST_SUFFIXES = [
'Test' => true,
'Mock' => false,
'Double' => false,
'test' => true,
'testcase' => true,
'mock' => false,
'double' => false,
];

/**
Expand Down Expand Up @@ -63,29 +64,28 @@ final class ObjectNameDepthSniff extends WPCS_Sniff {
* @return array<int|string>
*/
public function register() {
return [
\T_CLASS,
\T_INTERFACE,
\T_TRAIT,
];
$targets = Tokens::$ooScopeTokens;
unset( $targets[ \T_ANON_CLASS ] );

return $targets;
}

/**
* Processes this test, when one of its tokens is encountered.
*
* @param int $stackPtr The position of the current token
* in the stack passed in $tokens.
* @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 ) {
public function process( File $phpcsFile, $stackPtr ) {

// Check whether we are in a namespace or not.
if ( Namespaces::determineNamespace( $this->phpcsFile, $stackPtr ) === '' ) {
if ( Namespaces::determineNamespace( $phpcsFile, $stackPtr ) === '' ) {
return;
}

$object_name = ObjectDeclarations::getName( $this->phpcsFile, $stackPtr );
$object_name = ObjectDeclarations::getName( $phpcsFile, $stackPtr );
if ( empty( $object_name ) ) {
return;
}
Expand All @@ -95,72 +95,77 @@ public function process_token( $stackPtr ) {
// Handle names which are potentially in CamelCaps.
if ( \strpos( $snakecase_object_name, '_' ) === false ) {
$snakecase_object_name = SnakeCaseHelper::get_suggestion( $snakecase_object_name );

// Always treat "TestCase" as one word.
if ( \substr( $snakecase_object_name, -9 ) === 'test_case' ) {
$snakecase_object_name = \substr( $snakecase_object_name, 0, -9 ) . 'testcase';
}
}

$parts = \explode( '_', $snakecase_object_name );
$part_count = \count( $parts );

/*
* Allow the class name to be one part longer for confirmed test/mock/double classes.
* Allow the OO name to be one part longer for confirmed test/mock/double OO structures.
*/
$last = \array_pop( $parts );
$tokens = $phpcsFile->getTokens();

$last = \strtolower( \array_pop( $parts ) );
if ( isset( self::TEST_SUFFIXES[ $last ] ) ) {
if ( self::TEST_SUFFIXES[ $last ] === true && $this->is_test_class( $this->phpcsFile, $stackPtr ) ) {
if ( $tokens[ $stackPtr ]['code'] === \T_ENUM ) {
// Enums cannot extend, but a mock/double without direct link to the parent could be needed.
--$part_count;
}
else {
$extends = ObjectDeclarations::findExtendedClassName( $this->phpcsFile, $stackPtr );
$extends = ObjectDeclarations::findExtendedClassName( $phpcsFile, $stackPtr );
if ( \is_string( $extends ) ) {
--$part_count;
}
}
}

if ( $part_count <= $this->recommended_max_words && $part_count <= $this->max_words ) {
$this->phpcsFile->recordMetric( $stackPtr, 'Nr of words in object name', $part_count );
$phpcsFile->recordMetric( $stackPtr, 'Nr of words in object name', $part_count );
return;
}

// Check if the class is deprecated.
$ignore = [
\T_ABSTRACT => \T_ABSTRACT,
\T_FINAL => \T_FINAL,
\T_WHITESPACE => \T_WHITESPACE,
];
// Check if the OO structure is deprecated.
$ignore = Collections::classModifierKeywords();
$ignore[ \T_WHITESPACE ] = \T_WHITESPACE;

$comment_end = $stackPtr;
for ( $comment_end = ( $stackPtr - 1 ); $comment_end >= 0; $comment_end-- ) {
if ( isset( $ignore[ $this->tokens[ $comment_end ]['code'] ] ) === true ) {
if ( isset( $ignore[ $tokens[ $comment_end ]['code'] ] ) === true ) {
continue;
}

if ( $this->tokens[ $comment_end ]['code'] === \T_ATTRIBUTE_END
&& isset( $this->tokens[ $comment_end ]['attribute_opener'] ) === true
if ( $tokens[ $comment_end ]['code'] === \T_ATTRIBUTE_END
&& isset( $tokens[ $comment_end ]['attribute_opener'] ) === true
) {
$comment_end = $this->tokens[ $comment_end ]['attribute_opener'];
$comment_end = $tokens[ $comment_end ]['attribute_opener'];
continue;
}

break;
}

if ( $this->tokens[ $comment_end ]['code'] === \T_DOC_COMMENT_CLOSE_TAG ) {
// Only check if the class has a docblock.
$comment_start = $this->tokens[ $comment_end ]['comment_opener'];
foreach ( $this->tokens[ $comment_start ]['comment_tags'] as $tag ) {
if ( $this->tokens[ $tag ]['content'] === '@deprecated' ) {
// Deprecated class, ignore.
if ( $tokens[ $comment_end ]['code'] === \T_DOC_COMMENT_CLOSE_TAG ) {
// Only check if the OO structure has a docblock.
$comment_start = $tokens[ $comment_end ]['comment_opener'];
foreach ( $tokens[ $comment_start ]['comment_tags'] as $tag ) {
if ( $tokens[ $tag ]['content'] === '@deprecated' ) {
// Deprecated OO structure, ignore.
return;
}
}
}

$this->phpcsFile->recordMetric( $stackPtr, 'Nr of words in object name', $part_count );
$phpcsFile->recordMetric( $stackPtr, 'Nr of words in object name', $part_count );

// Active class.
$object_type = 'a ' . $this->tokens[ $stackPtr ]['content'];
if ( $this->tokens[ $stackPtr ]['code'] === \T_INTERFACE ) {
$object_type = 'an ' . $this->tokens[ $stackPtr ]['content'];
// Not a deprecated OO structure, this OO structure should comply with the rules.
$object_type = 'a ' . $tokens[ $stackPtr ]['content'];
if ( $tokens[ $stackPtr ]['code'] === \T_INTERFACE || $tokens[ $stackPtr ]['code'] === \T_ENUM ) {
$object_type = 'an ' . $tokens[ $stackPtr ]['content'];
}

if ( $part_count > $this->max_words ) {
Expand All @@ -172,7 +177,7 @@ public function process_token( $stackPtr ) {
$object_name,
];

$this->phpcsFile->addError( $error, $stackPtr, 'MaxExceeded', $data );
$phpcsFile->addError( $error, $stackPtr, 'MaxExceeded', $data );
}
elseif ( $part_count > $this->recommended_max_words ) {
$error = 'The name of %s should not consist of more than %d words. Words found: %d in %s';
Expand All @@ -183,7 +188,7 @@ public function process_token( $stackPtr ) {
$object_name,
];

$this->phpcsFile->addWarning( $error, $stackPtr, 'TooLong', $data );
$phpcsFile->addWarning( $error, $stackPtr, 'TooLong', $data );
}
}
}
42 changes: 39 additions & 3 deletions Yoast/Tests/NamingConventions/ObjectNameDepthUnitTest.2.inc
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ class Deprecated_Class_With_Too_Long_Class_Name {} // OK.
* @since x.x.x
*/
#[Some_Attribute]
class Active_Class_With_Attribute_With_Too_Long_Class_Name {} // Error.
final class Active_Class_With_Attribute_With_Too_Long_Class_Name {} // Error.

/**
* Class description.
Expand All @@ -79,15 +79,15 @@ class Active_Class_With_Attribute_With_Too_Long_Class_Name {} // Error.
*/
#[Attribute_One]
#[Attribute_Two]
class Deprecated_Class_With_Attribute_With_Too_Long_Class_Name {} // OK.
abstract class Deprecated_Class_With_Attribute_With_Too_Long_Class_Name {} // OK.

/*
* Allow for a `_Test` suffix in classes within the unit test suite.
*/
class Three_Word_Name_Test {} // Error.

class Three_Word_Name_Test extends TestCase {} // OK.
class Three_Word_Name_Test extends WP_UnitTestCase {} // OK.
class Three_Word_Name_Test extends \WP_UnitTestCase {} // OK.

class Four_Word_Long_Name_Test extends TestCase {} // Error.

Expand All @@ -114,3 +114,39 @@ trait SomeACRONYMName {} // Error - false positive, this will be fixed in a late
class TooLongClassName {} // Error.
interface ThisInterfaceNameIsTooLong {} // Error.
trait TraitNameIsTooLong {} // Error.

// Now, let's also make sure the test/double handling works for CamelCaps class names + class names with the suffix in an unconventional case.
class ThreeWordNameTest extends TestCase {} // OK.
class ThreeWordNameDouble extends ThreeWordName {} // OK.
class Three_Word_Name_test extends TestCase {} // OK.
class Three_Word_Name_MOCK extends Some_Class {} // OK.

/*
* TestCase classes should also be allowed extra word length.
*/
class ThreeWordNameTestCase extends TestCase {} // OK.
class Three_Word_Name_TestCase extends TestCase {} // OK.

/*
* Allow for PHP 8.1+ enums.
*/
enum My_Enum_Name {} // OK.
enum MyEnumName {} // OK.
enum Too_Long_Enum_Name: int {} // Error.
enum TooLongEnumName: int {} // Error.
enum Three_Word_Name_Mock: string implements Word_Interface {} // OK.

/**
* Class description, no @deprecated tag.
*
* @since x.x.x
*/
#[Some_Attribute]
readonly class Active_Class_With_Attribute_With_Too_Long_Class_Name {} // Error.

/**
* Class description.
*
* @deprecated x.x.x
*/
final readonly class Deprecated_Readonly_Class_With_Too_Long_Class_Name {} // OK.
7 changes: 7 additions & 0 deletions Yoast/Tests/NamingConventions/ObjectNameDepthUnitTest.3.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

namespace Foo;

// Live coding/parse error test.
// This must be the only/last test in the file.
class
3 changes: 3 additions & 0 deletions Yoast/Tests/NamingConventions/ObjectNameDepthUnitTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ public function getErrorList( string $testFile = '' ): array {
114 => 1,
115 => 1,
116 => 1,
135 => 1,
136 => 1,
145 => 1,
];

default:
Expand Down

0 comments on commit bc72022

Please sign in to comment.