Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 41
0.00% covered (danger)
0.00%
0 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
LowerCamelFunctionsNameSniff
0.00% covered (danger)
0.00%
0 / 41
0.00% covered (danger)
0.00%
0 / 3
342
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 / 24
0.00% covered (danger)
0.00%
0 / 1
132
 shouldIgnoreHookHandler
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
42
1<?php
2
3namespace MediaWiki\Sniffs\NamingConventions;
4
5use MediaWiki\Sniffs\PHPUnit\PHPUnitTestTrait;
6use PHP_CodeSniffer\Files\File;
7use PHP_CodeSniffer\Sniffs\Sniff;
8use PHP_CodeSniffer\Util\Tokens;
9
10/**
11 * Make sure function names follow lower camel case
12 *
13 * This ignores methods in the form on(Something) where the class
14 * implements an interface with the name (Something)Hook, to avoid
15 * sending warnings for code in MediaWiki extensions and skins for
16 * hook handlers where the method cannot be renamed because it is
17 * inherited from the hook interface
18 *
19 * @author DannyS712
20 */
21class LowerCamelFunctionsNameSniff implements Sniff {
22
23    use PHPUnitTestTrait;
24
25    // Magic methods.
26    private const MAGIC_METHODS = [
27        '__construct' => true,
28        '__destruct' => true,
29        '__call' => true,
30        '__callstatic' => true,
31        '__get' => true,
32        '__set' => true,
33        '__isset' => true,
34        '__unset' => true,
35        '__sleep' => true,
36        '__wakeup' => true,
37        '__tostring' => true,
38        '__set_state' => true,
39        '__clone' => true,
40        '__invoke' => true,
41        '__serialize' => true,
42        '__unserialize' => true,
43        '__debuginfo' => true
44    ];
45
46    // A list of non-magic methods with double underscore.
47    private const METHOD_DOUBLE_UNDERSCORE = [
48        '__soapcall' => true,
49        '__getlastrequest' => true,
50        '__getlastresponse' => true,
51        '__getlastrequestheaders' => true,
52        '__getlastresponseheaders' => true,
53        '__getfunctions' => true,
54        '__gettypes' => true,
55        '__dorequest' => true,
56        '__setcookie' => true,
57        '__setlocation' => true,
58        '__setsoapheaders' => true
59    ];
60
61    /**
62     * @inheritDoc
63     */
64    public function register(): array {
65        return [ T_FUNCTION ];
66    }
67
68    /**
69     * @param File $phpcsFile
70     * @param int $stackPtr The current token index.
71     * @return void
72     */
73    public function process( File $phpcsFile, $stackPtr ) {
74        $originalFunctionName = $phpcsFile->getDeclarationName( $stackPtr );
75        if ( $originalFunctionName === null ) {
76            return;
77        }
78
79        $lowerFunctionName = strtolower( $originalFunctionName );
80        if ( isset( self::METHOD_DOUBLE_UNDERSCORE[$lowerFunctionName] ) ||
81            isset( self::MAGIC_METHODS[$lowerFunctionName] )
82        ) {
83            // Method is excluded from this sniff
84            return;
85        }
86
87        $containsUnderscores = str_contains( $originalFunctionName, '_' );
88        if ( $originalFunctionName[0] === $lowerFunctionName[0] &&
89            ( !$containsUnderscores || $this->isTestFunction( $phpcsFile, $stackPtr ) )
90        ) {
91            // Everything is ok when the first letter is lowercase and there are no underscores
92            // (except in tests where they are allowed)
93            return;
94        }
95
96        if ( $containsUnderscores ) {
97            // Check for MediaWiki hooks
98            // Only matters if there is an underscore, all hook handlers have methods beginning
99            // with "on" and so start with lowercase
100            if ( $this->shouldIgnoreHookHandler( $phpcsFile, $stackPtr, $originalFunctionName ) ) {
101                return;
102            }
103        }
104
105        $tokens = $phpcsFile->getTokens();
106        foreach ( $tokens[$stackPtr]['conditions'] as $code ) {
107            if ( !isset( Tokens::$ooScopeTokens[$code] ) ) {
108                continue;
109            }
110
111            $phpcsFile->addError(
112                'Method name "%s" should use lower camel case.',
113                $stackPtr,
114                'FunctionName',
115                [ $originalFunctionName ]
116            );
117        }
118    }
119
120    /**
121     * Check if the method should be ignored because it is a hook handler and the method
122     * name is inherited from an interface
123     *
124     * @param File $phpcsFile
125     * @param int $stackPtr
126     * @param string $functionName
127     * @return bool
128     */
129    private function shouldIgnoreHookHandler(
130        File $phpcsFile,
131        int $stackPtr,
132        string $functionName
133    ): bool {
134        $matches = [];
135        if ( !( preg_match( '/^on([A-Z]\S+)$/', $functionName, $matches ) ) ) {
136            return false;
137        }
138
139        // Method name looks like a hook handler, check if the class implements
140        // a hook by that name
141
142        $classToken = $this->getClassToken( $phpcsFile, $stackPtr );
143        if ( !$classToken ) {
144            // Not within a class, don't skip
145            return false;
146        }
147
148        $implementedInterfaces = $phpcsFile->findImplementedInterfaceNames( $classToken );
149        if ( !$implementedInterfaces ) {
150            // Not implementing the hook interface
151            return false;
152        }
153
154        $hookMethodName = $matches[1];
155        $hookInterfaceName = $hookMethodName . 'Hook';
156
157        // We need to account for the interface name in both the fully qualified form,
158        // and just the interface name. If we have the fully qualified form, explode()
159        // will return an array of the different namespaces and sub namespaces, with the
160        // last entry being the actual interface name, and if we just have the interface
161        // name, explode() will return an array of just that string
162        foreach ( $implementedInterfaces as $interface ) {
163            $parts = explode( '\\', $interface );
164            if ( end( $parts ) === $hookInterfaceName ) {
165                return true;
166            }
167        }
168        return false;
169    }
170
171}