1717
1818namespace MoodleHQ \MoodleCS \moodle \Sniffs \PHPUnit ;
1919
20- use MoodleHQ \MoodleCS \moodle \Util \MoodleUtil ;
21- use PHP_CodeSniffer \Sniffs \Sniff ;
20+ use MoodleHQ \MoodleCS \moodle \Util \Attributes ;
2221use PHP_CodeSniffer \Files \File ;
2322use PHP_CodeSniffer \Util \Tokens ;
2423use PHPCSUtils \Utils \ObjectDeclarations ;
2928 * @copyright 2022 onwards Eloy Lafuente (stronk7) {@link https://stronk7.com}
3029 * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3130 */
32- class TestCaseCoversSniff implements Sniff
31+ class TestCaseCoversSniff extends AbstractTestCaseSniff
3332{
3433 /**
3534 * Register for open tag (only process once per file).
@@ -45,18 +44,12 @@ public function register() {
4544 * @param int $pointer The position in the stack.
4645 */
4746 public function process (File $ file , $ pointer ) {
48- // Before starting any check, let's look for various things.
49-
50- // If we aren't checking Moodle 4.0dev (400) and up, nothing to check.
51- // Make and exception for codechecker phpunit tests, so they are run always.
52- if (!MoodleUtil::meetsMinimumMoodleVersion ($ file , 400 ) && !MoodleUtil::isUnitTestRunning ()) {
47+ if (!$ this ->shouldCheckFile ($ file )) {
48+ // Nothing to check.
5349 return ; // @codeCoverageIgnore
5450 }
5551
56- // If the file is not a unit test file, nothing to check.
57- if (!MoodleUtil::isUnitTest ($ file ) && !MoodleUtil::isUnitTestRunning ()) {
58- return ; // @codeCoverageIgnore
59- }
52+ $ supportsAttributes = $ this ->shouldCheckTestCaseAttributes ($ file );
6053
6154 // We have all we need from core, let's start processing the file.
6255
@@ -73,29 +66,44 @@ public function process(File $file, $pointer) {
7366 return ; // @codeCoverageIgnore
7467 }
7568
76- // Iterate over all the classes (hopefully only one, but that's not this sniff problem).
77- $ cStart = $ pointer ;
78- while ($ cStart = $ file ->findNext (T_CLASS , $ cStart + 1 )) {
69+ foreach ($ this ->getTestCasesInFile ($ file , $ pointer ) as $ cStart => $ className ) {
7970 $ classInfo = ObjectDeclarations::getClassProperties ($ file , $ cStart );
8071 if ($ classInfo ['is_abstract ' ]) {
8172 // Abstract classes are not tested.
8273 // Coverage information belongs to the concrete classes that extend them.
8374 continue ;
8475 }
76+
77+
8578 $ class = $ file ->getDeclarationName ($ cStart );
8679 $ classCovers = false ; // To control when the class has a @covers tag.
8780 $ classCoversNothing = false ; // To control when the class has a @coversNothing tag.
8881 $ classCoversDefaultClass = []; // To annotate all the existing @coversDefaultClass tags.
8982
90- // Only if the class is extending something.
91- // TODO: We could add a list of valid classes once we have a class-map available.
92- if (!$ file ->findNext (T_EXTENDS , $ cStart + 1 , $ tokens [$ cStart ]['scope_opener ' ])) {
93- continue ;
94- }
95-
96- // Ignore non ended "_test|_testcase" classes.
97- if (substr ($ class , -5 ) !== '_test ' && substr ($ class , -9 ) != '_testcase ' ) {
98- continue ;
83+ if ($ supportsAttributes ) {
84+ // From PHPUnit 10 onwards, the class may be annotated with attributes.
85+ // The following attributes exist:
86+ // - #[\PHPUnit\Framework\Attributes\CoversClass]
87+ // - #[\PHPUnit\Framework\Attributes\CoversTrait]
88+ // - #[\PHPUnit\Framework\Attributes\CoversMethod]
89+ // - #[\PHPUnit\Framework\Attributes\CoversFunction]
90+ // - #[\PHPUnit\Framework\Attributes\CoversNothing]
91+
92+ // All of these may be at the class level.
93+ // Only the `CoversNothing` attribute is allowed to be used at the method level.
94+
95+ // Check if any valid Covers attributes are defined for this class.
96+ $ validAttributes = [
97+ \PHPUnit \Framework \Attributes \CoversClass::class,
98+ \PHPUnit \Framework \Attributes \CoversTrait::class,
99+ \PHPUnit \Framework \Attributes \CoversMethod::class,
100+ \PHPUnit \Framework \Attributes \CoversFunction::class,
101+ \PHPUnit \Framework \Attributes \CoversNothing::class,
102+ ];
103+ if ($ this ->containsCoversAttribute ($ file , $ cStart , $ validAttributes )) {
104+ // If the class has any of the valid attributes, we can skip the rest of the checks.
105+ continue ;
106+ }
99107 }
100108
101109 // Let's see if the class has any phpdoc block (first non skip token must be end of phpdoc comment).
@@ -163,16 +171,24 @@ public function process(File $file, $pointer) {
163171 }
164172
165173 // Iterate over all the methods in the class.
166- $ mStart = $ cStart ;
167- while ($ mStart = $ file ->findNext (T_FUNCTION , $ mStart + 1 , $ tokens [$ cStart ]['scope_closer ' ])) {
168- $ method = $ file ->getDeclarationName ($ mStart );
174+ // From PHPUnit 10 onwards, the class may be annotated with attributes.
175+ // The following method-level attributes exist:
176+ // - #[\PHPUnit\Framework\Attributes\CoversNothing]
177+ $ validAttributes = [
178+ \PHPUnit \Framework \Attributes \CoversNothing::class,
179+ ];
180+ foreach ($ this ->getTestMethodsInClass ($ file , $ cStart ) as $ method => $ mStart ) {
181+ if ($ supportsAttributes ) {
182+ // Check if any valid Covers attributes are defined for this class.
183+ if ($ this ->containsCoversAttribute ($ file , $ mStart , $ validAttributes )) {
184+ // If the class has any of the valid attributes, we can skip the rest of the checks.
185+ continue ;
186+ }
187+ }
188+
169189 $ methodCovers = false ; // To control when the method has a @covers tag.
170190 $ methodCoversNothing = false ; // To control when the method has a @coversNothing tag.
171191
172- // Ignore non test_xxxx() methods.
173- if (strpos ($ method , 'test_ ' ) !== 0 ) {
174- continue ;
175- }
176192
177193 // Let's see if the method has any phpdoc block (first non skip token must be end of phpdoc comment).
178194 $ docPointer = $ file ->findPrevious ($ skipTokens , $ mStart - 1 , null , true );
@@ -347,4 +363,35 @@ protected function checkCoversTagsSyntax(File $file, int $pointer, string $tag)
347363 }
348364 }
349365 }
366+
367+ /**
368+ * Checks if the class contains any of the valid @coversXXX attributes.
369+ *
370+ * @param File $file The file being scanned.
371+ * @param int $pointer The position in the stack.
372+ * @param array $validAttributes List of valid attributes to check against.
373+ * @return bool True if any valid attribute is found, false otherwise.
374+ */
375+ protected function containsCoversAttribute (
376+ File $ file ,
377+ int $ pointer ,
378+ array $ validAttributes
379+ ): bool {
380+ // Find all the attributes for this class.
381+ $ attributes = Attributes::getAttributePointers ($ file , $ pointer );
382+ foreach ($ attributes as $ attributePtr ) {
383+ $ attribute = Attributes::getAttributeProperties ($ file , $ attributePtr );
384+ if ($ attribute === null ) {
385+ // No attribute found, skip.
386+ continue ; // @codeCoverageIgnore
387+ }
388+
389+ if (in_array ($ attribute ['qualified_name ' ], $ validAttributes )) {
390+ // Valid attribute found.
391+ return true ;
392+ }
393+ }
394+
395+ return false ;
396+ }
350397}
0 commit comments