Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 44
0.00% covered (danger)
0.00%
0 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
MissingCoversSniff
0.00% covered (danger)
0.00%
0 / 44
0.00% covered (danger)
0.00%
0 / 3
240
0.00% covered (danger)
0.00%
0 / 1
 register
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 process
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
72
 hasCoversTags
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
42
1<?php
2/**
3 * Copyright (C) 2015 WordPoints
4 * Copyright (C) 2018 Kunal Mehta <legoktm@member.fsf.org>
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License along
17 * with this program; if not, write to the Free Software Foundation, Inc.,
18 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19 */
20
21namespace MediaWiki\Sniffs\Commenting;
22
23use PHP_CodeSniffer\Files\File;
24use PHP_CodeSniffer\Sniffs\Sniff;
25use PHP_CodeSniffer\Util\Tokens;
26
27/**
28 * Identify Test classes that do not have
29 * any @covers tags
30 */
31class MissingCoversSniff implements Sniff {
32
33    /**
34     * @inheritDoc
35     */
36    public function register(): array {
37        return [ T_CLASS ];
38    }
39
40    /**
41     * @param File $phpcsFile
42     * @param int $stackPtr Position of T_CLASS
43     * @return void
44     */
45    public function process( File $phpcsFile, $stackPtr ) {
46        $name = $phpcsFile->getDeclarationName( $stackPtr );
47        if ( !str_ends_with( $name, 'Test' ) ) {
48            // Only want to validate classes that end in test
49            return;
50        }
51        $props = $phpcsFile->getClassProperties( $stackPtr );
52        if ( $props['is_abstract'] ) {
53            // No point in requiring @covers from an abstract class
54            return;
55        }
56
57        $classCovers = $this->hasCoversTags( $phpcsFile, $stackPtr );
58        if ( $classCovers ) {
59            // The class has a @covers tag, awesome.
60            return;
61        }
62
63        // Check each individual test function
64        $tokens = $phpcsFile->getTokens();
65        $classCloser = $tokens[$stackPtr]['scope_closer'];
66        $funcPtr = $stackPtr;
67        while ( true ) {
68            $funcPtr = $phpcsFile->findNext( [ T_FUNCTION ], $funcPtr + 1, $classCloser );
69            if ( !$funcPtr ) {
70                // No more
71                break;
72            }
73
74            $name = $phpcsFile->getDeclarationName( $funcPtr );
75            if ( !str_starts_with( $name, 'test' ) ) {
76                // If it doesn't start with "test", skip
77                continue;
78            }
79
80            $hasCovers = $this->hasCoversTags( $phpcsFile, $funcPtr );
81            if ( !$hasCovers ) {
82                $phpcsFile->addWarning(
83                    'The %s test method has no @covers tags',
84                    $funcPtr, 'MissingCovers', [ $name ]
85                );
86            }
87        }
88    }
89
90    /**
91     * Whether the statement has @covers tags
92     *
93     * @param File $phpcsFile
94     * @param int $stackPtr Position of T_CLASS/T_FUNCTION
95     *
96     * @return bool
97     */
98    protected function hasCoversTags( File $phpcsFile, int $stackPtr ): bool {
99        $exclude = array_merge(
100            Tokens::$methodPrefixes,
101            [ T_WHITESPACE ]
102        );
103        $closer = $phpcsFile->findPrevious( $exclude, $stackPtr - 1, 0, true );
104        if ( $closer === false ) {
105            return false;
106        }
107        $tokens = $phpcsFile->getTokens();
108        $token = $tokens[$closer];
109        if ( $token['code'] !== T_DOC_COMMENT_CLOSE_TAG ) {
110            // No doc comment
111            return false;
112        }
113
114        $opener = $tokens[$closer]['comment_opener'];
115        $tags = $tokens[$opener]['comment_tags'];
116        foreach ( $tags as $tag ) {
117            $name = $tokens[$tag]['content'];
118            if ( $name === '@covers' || $name === '@coversNothing' ) {
119                return true;
120            }
121        }
122
123        return false;
124    }
125
126}