Skip to content

Commit

Permalink
Merge pull request #331 from Yoast/JRF/yoastcs-testshavecoverstag-var…
Browse files Browse the repository at this point in the history
…ious-improvements

Commenting/TestsHaveCoversTag: support PHP 8.1 + 8.2, bug fix and various other improvements
  • Loading branch information
jrfnl authored Nov 4, 2023
2 parents 9d00928 + d36d2ae commit 0a366ae
Show file tree
Hide file tree
Showing 7 changed files with 124 additions and 28 deletions.
41 changes: 26 additions & 15 deletions Yoast/Sniffs/Commenting/TestsHaveCoversTagSniff.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Util\Tokens;
use PHPCSUtils\Tokens\Collections;
use PHPCSUtils\Utils\FunctionDeclarations;
use PHPCSUtils\Utils\ObjectDeclarations;
use PHPCSUtils\Utils\Scopes;

/**
* Verifies that all test functions have at least one @covers tag.
Expand Down Expand Up @@ -45,9 +47,8 @@ public function process( File $phpcsFile, $stackPtr ) {
return $this->process_class( $phpcsFile, $stackPtr );
}

if ( $tokens[ $stackPtr ]['code'] === \T_FUNCTION ) {
return $this->process_function( $phpcsFile, $stackPtr );
}
// This must be a T_FUNCTION token.
$this->process_function( $phpcsFile, $stackPtr );
}

/**
Expand All @@ -59,13 +60,14 @@ public function process( File $phpcsFile, $stackPtr ) {
* @return void|int If covers annotations were found (or this is not a test class),
* will return the stack pointer to the end of the class.
*/
protected function process_class( File $phpcsFile, $stackPtr ) {
private function process_class( File $phpcsFile, $stackPtr ) {
$tokens = $phpcsFile->getTokens();
$name = ObjectDeclarations::getName( $phpcsFile, $stackPtr );

if ( \substr( $name, -4 ) !== 'Test'
&& \substr( $name, -8 ) !== 'TestCase'
&& \substr( $name, 0, 4 ) !== 'Test'
if ( empty( $name )
|| ( \substr( $name, -4 ) !== 'Test'
&& \substr( $name, -8 ) !== 'TestCase'
&& \substr( $name, 0, 4 ) !== 'Test' )
) {
// Not a test class.
if ( isset( $tokens[ $stackPtr ]['scope_closer'] ) ) {
Expand All @@ -76,12 +78,9 @@ protected function process_class( File $phpcsFile, $stackPtr ) {
return;
}

// @todo: Once PHPCSUtils is out, replace with call to new findCommentAboveOOStructure() method.
$ignore = [
\T_WHITESPACE => \T_WHITESPACE,
\T_ABSTRACT => \T_ABSTRACT,
\T_FINAL => \T_FINAL,
];
// @todo: Once PHPCSUtils 1.2.0 (?) is out, replace with call to new findCommentAboveOOStructure() method.
$ignore = Collections::classModifierKeywords();
$ignore[ \T_WHITESPACE ] = \T_WHITESPACE;

$commentEnd = $stackPtr;
for ( $commentEnd = ( $stackPtr - 1 ); $commentEnd >= 0; $commentEnd-- ) {
Expand Down Expand Up @@ -111,10 +110,12 @@ protected function process_class( File $phpcsFile, $stackPtr ) {
foreach ( $tokens[ $commentStart ]['comment_tags'] as $tag ) {
if ( $tokens[ $tag ]['content'] === '@covers' ) {
$foundCovers = true;
break;
}

if ( $tokens[ $tag ]['content'] === '@coversNothing' ) {
$foundCoversNothing = true;
break;
}
}

Expand All @@ -134,10 +135,15 @@ protected function process_class( File $phpcsFile, $stackPtr ) {
*
* @return void
*/
protected function process_function( File $phpcsFile, $stackPtr ) {
private function process_function( File $phpcsFile, $stackPtr ) {
$tokens = $phpcsFile->getTokens();

// @todo: Once PHPCSUtils is out, replace with call to new findCommentAboveFunction() method.
if ( Scopes::isOOMethod( $phpcsFile, $stackPtr ) === false ) {
// This is a global function, not a method in a test class.
return;
}

// @todo: Once PHPCSUtils 1.2.0 (?) is out, replace with call to new findCommentAboveFunction() method.
$ignore = Tokens::$methodPrefixes;
$ignore[ \T_WHITESPACE ] = \T_WHITESPACE;

Expand Down Expand Up @@ -183,6 +189,11 @@ protected function process_function( File $phpcsFile, $stackPtr ) {
}

$name = FunctionDeclarations::getName( $phpcsFile, $stackPtr );
if ( empty( $name ) ) {
// Parse error. Ignore this method as it will never be run as a test.
return;
}

if ( \stripos( $name, 'test' ) !== 0 && $foundTest === false ) {
// Not a test method.
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class ClassLevelCoversTest {
/**
* @coversNothing
*/
class ClassLevelCoversNothingTest {
final class ClassLevelCoversNothingTest {

/**
* Docblock.
Expand Down Expand Up @@ -85,7 +85,7 @@ class ClassNameTest {
*
* @test
*/
public function annotatedTestMissingCoversTag() {}
public static function annotatedTestMissingCoversTag() {}

/**
* Docblock.
Expand All @@ -101,14 +101,14 @@ class ClassNameTest {
* @test
* @covers ::globalFunction
*/
public function annotatedTestHasCoversTag() {}
final public function annotatedTestHasCoversTag() {}
}

/**
* @covers ClassName
*/
#[Some_Attribute]
class ClassLevelCoversWithAttributeTest {
final class ClassLevelCoversWithAttributeTest {

/**
* Docblock.
Expand Down Expand Up @@ -154,3 +154,44 @@ abstract class ContainsTestMethodsTestCase {
*/
abstract function testToBeImplementedInChildClass();
}

/**
* Docblock.
*
* @sometag
* @anothertag
*/
class DoesNotHaveOurTargetTagsTest {
/**
* Docblock with tags, but not any of the tags we're looking for.
*
* @param mixed $input
* @param string $expected
*
* @return void
*/
final public function testSomething($input, $expected) {}
}

/**
* Global functions should be ignored as PHPUnit tests should always be in a class.
*/
function testMe() {}

/**
* Docblocks above PHP 8.2 readonly test classes should be recognized correctly.
*
* @covers \Some\Class::methodName
*/
readonly class ReadonlyTest {
public function testMe() {}
}

/**
* Docblocks above PHP 8.2 readonly test classes should be recognized correctly.
*
* @covers \Some\Class::methodName
*/
final readonly class FinalReadonlyTest {
public function testMe() {}
}
5 changes: 5 additions & 0 deletions Yoast/Tests/Commenting/TestsHaveCoversTagUnitTest.2.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php

// Live coding/parse error test.
// This must be the only test in the file.
class NotATestClass {
13 changes: 13 additions & 0 deletions Yoast/Tests/Commenting/TestsHaveCoversTagUnitTest.3.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

// Live coding/parse error test.
// This must be the only test in the file.

/**
* @covers Something
*/
class LiveCodingTest {
/**
* @covers SomethingElse
*/
public function testThis() {}
5 changes: 5 additions & 0 deletions Yoast/Tests/Commenting/TestsHaveCoversTagUnitTest.4.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php

// Live coding/parse error test.
// This must be the only test in the file.
final class
11 changes: 11 additions & 0 deletions Yoast/Tests/Commenting/TestsHaveCoversTagUnitTest.5.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

// Live coding/parse error test.
// This must be the only test in the file.

class ParseErrorTest {
/**
* This function is missing its name. This parse error should be ignored.
*/
public function {}
}
28 changes: 19 additions & 9 deletions Yoast/Tests/Commenting/TestsHaveCoversTagUnitTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,27 @@ final class TestsHaveCoversTagUnitTest extends AbstractSniffUnitTest {
/**
* Returns the lines where errors should occur.
*
* @param string $testFile The name of the file being tested.
*
* @return array<int, int> Key is the line number, value is the number of expected errors.
*/
public function getErrorList(): array {
return [
49 => 1,
59 => 1,
88 => 1,
126 => 1,
142 => 1,
150 => 1,
];
public function getErrorList( string $testFile = '' ): array {

switch ( $testFile ) {
case 'TestsHaveCoversTagUnitTest.1.inc':
return [
49 => 1,
59 => 1,
88 => 1,
126 => 1,
142 => 1,
150 => 1,
173 => 1,
];

default:
return [];
}
}

/**
Expand Down

0 comments on commit 0a366ae

Please sign in to comment.