Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 73
0.00% covered (danger)
0.00%
0 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
AssertEqualsSniff
0.00% covered (danger)
0.00%
0 / 73
0.00% covered (danger)
0.00%
0 / 2
1406
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 / 72
0.00% covered (danger)
0.00%
0 / 1
1332
1<?php
2
3namespace MediaWiki\Sniffs\PHPUnit;
4
5use PHP_CodeSniffer\Files\File;
6use PHP_CodeSniffer\Sniffs\Sniff;
7
8/**
9 * Discourages the use of PHPUnit's relaxed, not type-safe assertEquals() in favor of strict
10 * alternatives like assertSame(), assertNull(), and such. Please note: The auto-fixes done by this
11 * sniff can make PHPUnit test cases fail. These should be fixed not by reverting the fix, but by
12 * finding better expected values or better assertions.
13 *
14 * @author Thiemo Kreuz
15 * @license GPL-2.0-or-later
16 */
17class AssertEqualsSniff implements Sniff {
18    use PHPUnitTestTrait;
19
20    private const ASSERTIONS = [
21        'assertEquals' => true,
22        'assertNotEquals' => true,
23        'assertNotSame' => true,
24    ];
25
26    /**
27     * @inheritDoc
28     */
29    public function register(): array {
30        return [ T_STRING ];
31    }
32
33    /**
34     * @param File $phpcsFile
35     * @param int $stackPtr
36     *
37     * @return void|int
38     */
39    public function process( File $phpcsFile, $stackPtr ) {
40        if ( !$this->isTestFile( $phpcsFile, $stackPtr ) ) {
41            return $phpcsFile->numTokens;
42        }
43
44        $tokens = $phpcsFile->getTokens();
45        $assertion = $tokens[$stackPtr]['content'];
46
47        // We don't care about stuff that's not in a method in a class
48        if ( $tokens[$stackPtr]['level'] < 2 || !isset( self::ASSERTIONS[$assertion] ) ) {
49            return;
50        }
51
52        $opener = $phpcsFile->findNext( T_WHITESPACE, $stackPtr + 1, null, true );
53        // Looks like this string is not a method call
54        if ( !isset( $tokens[$opener]['parenthesis_closer'] ) ) {
55            return $opener;
56        }
57
58        $isAssertEquals = $assertion === 'assertEquals';
59        $expected = $phpcsFile->findNext( T_WHITESPACE, $opener + 1, null, true );
60        $msg = '%s accepts many non-%s values, please use strict alternatives like %s';
61        /** @var bool|string $fix */
62        $fix = false;
63
64        switch ( $tokens[$expected]['code'] ) {
65            case T_NULL:
66                if ( !$isAssertEquals ) {
67                    break;
68                }
69
70                $msgParams = [ $assertion, 'null', 'assertNull' ];
71                if ( $phpcsFile->addFixableWarning( $msg, $stackPtr, 'Null', $msgParams ) ) {
72                    $fix = 'assertNull';
73                }
74                break;
75
76            case T_FALSE:
77                $replacement = $isAssertEquals ? 'assertFalse' : 'assertTrue';
78                $msgParams = [ $assertion, $isAssertEquals ? 'false' : 'true', $replacement ];
79                if ( $phpcsFile->addFixableWarning( $msg, $stackPtr, 'False', $msgParams ) ) {
80                    $fix = $replacement;
81                }
82                break;
83
84            case T_TRUE:
85                $replacement = $isAssertEquals ? 'assertTrue' : 'assertFalse';
86                $msgParams = [ $assertion, $isAssertEquals ? 'true' : 'false', $replacement ];
87                if ( $phpcsFile->addFixableWarning( $msg, $stackPtr, 'True', $msgParams ) ) {
88                    $fix = $replacement;
89                }
90                break;
91
92            case T_LNUMBER:
93                if ( !$isAssertEquals ) {
94                    break;
95                }
96
97                $number = (int)$tokens[$expected]['content'];
98                if ( $number === 0 || $number === 1 ) {
99                    $msgParams = [ $assertion, $number ? 'numeric' : 'zero', 'assertSame' ];
100                    $fix = $phpcsFile->addFixableWarning( $msg, $stackPtr, 'Int', $msgParams );
101                }
102                break;
103
104            case T_DNUMBER:
105                if ( !$isAssertEquals ) {
106                    break;
107                }
108
109                $number = (float)$tokens[$expected]['content'];
110                if ( $number === 0.0 || $number === 1.0 ) {
111                    $msgParams = [ $assertion, $number ? 'numeric' : 'zero', 'assertSame' ];
112                    $fix = $phpcsFile->addFixableWarning( $msg, $stackPtr, 'Float', $msgParams );
113                }
114                break;
115
116            case T_CONSTANT_ENCAPSED_STRING:
117                if ( !$isAssertEquals ) {
118                    break;
119                }
120
121                $msgParams = [ $assertion, 'string', 'assertSame' ];
122
123                // The empty string as well as "0" are among PHP's "falsy" values
124                if ( strlen( $tokens[$expected]['content'] ) <= 2 ||
125                    $tokens[$expected]['content'][1] === '0'
126                ) {
127                    $fix = $phpcsFile->addFixableWarning( $msg, $stackPtr, 'FalsyString', $msgParams );
128                    break;
129                }
130
131                $string = trim( substr( $tokens[$expected]['content'], 1, -1 ) );
132                if ( ctype_digit( $string ) ) {
133                    $fix = $phpcsFile->addFixableWarning( $msg, $stackPtr, 'IntegerString', $msgParams );
134                } elseif ( is_numeric( $string ) ) {
135                    $fix = $phpcsFile->addFixableWarning( $msg, $stackPtr, 'NumericString', $msgParams );
136                }
137        }
138
139        $fixer = $phpcsFile->fixer;
140        // Fall back to assertSame instead of blindly removing unknown tokens
141        if ( is_string( $fix ) && $tokens[$expected + 1]['code'] === T_COMMA ) {
142            $fixer->replaceToken( $stackPtr, $fix );
143            $fixer->replaceToken( $expected, '' );
144            $fixer->replaceToken( $expected + 1, '' );
145            if ( $tokens[$expected + 2]['code'] === T_WHITESPACE ) {
146                $fixer->replaceToken( $expected + 2, '' );
147            }
148        } elseif ( $fix ) {
149            $fixer->replaceToken( $stackPtr, 'assertSame' );
150        }
151
152        // There is no way the next assertEquals() can be closer than this
153        return $tokens[$opener]['parenthesis_closer'] + 4;
154    }
155
156}