1<?php
2/**
3 * PEAR_Sniffs_NamingConventions_ValidFunctionNameSniff.
4 *
5 * PHP version 5
6 *
7 * @category  PHP
8 * @package   PHP_CodeSniffer
9 * @author    Greg Sherwood <gsherwood@squiz.net>
10 * @author    Marc McIntyre <mmcintyre@squiz.net>
11 * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600)
12 * @license   https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
13 * @link      http://pear.php.net/package/PHP_CodeSniffer
14 */
15
16if (class_exists('PHP_CodeSniffer_Standards_AbstractScopeSniff', true) === false) {
17    throw new PHP_CodeSniffer_Exception('Class PHP_CodeSniffer_Standards_AbstractScopeSniff not found');
18}
19
20/**
21 * PEAR_Sniffs_NamingConventions_ValidFunctionNameSniff.
22 *
23 * Ensures method names are correct depending on whether they are public
24 * or private, and that functions are named correctly.
25 *
26 * @category  PHP
27 * @package   PHP_CodeSniffer
28 * @author    Greg Sherwood <gsherwood@squiz.net>
29 * @author    Marc McIntyre <mmcintyre@squiz.net>
30 * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600)
31 * @license   https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
32 * @version   Release: @package_version@
33 * @link      http://pear.php.net/package/PHP_CodeSniffer
34 */
35class PEAR_Sniffs_NamingConventions_ValidFunctionNameSniff extends PHP_CodeSniffer_Standards_AbstractScopeSniff
36{
37
38    /**
39     * A list of all PHP magic methods.
40     *
41     * @var array
42     */
43    protected $magicMethods = array(
44                               'construct'  => true,
45                               'destruct'   => true,
46                               'call'       => true,
47                               'callstatic' => true,
48                               'get'        => true,
49                               'set'        => true,
50                               'isset'      => true,
51                               'unset'      => true,
52                               'sleep'      => true,
53                               'wakeup'     => true,
54                               'tostring'   => true,
55                               'set_state'  => true,
56                               'clone'      => true,
57                               'invoke'     => true,
58                               'debuginfo'  => true,
59                              );
60
61    /**
62     * A list of all PHP magic functions.
63     *
64     * @var array
65     */
66    protected $magicFunctions = array('autoload' => true);
67
68
69    /**
70     * Constructs a PEAR_Sniffs_NamingConventions_ValidFunctionNameSniff.
71     */
72    public function __construct()
73    {
74        parent::__construct(array(T_CLASS, T_ANON_CLASS, T_INTERFACE, T_TRAIT), array(T_FUNCTION), true);
75
76    }//end __construct()
77
78
79    /**
80     * Processes the tokens within the scope.
81     *
82     * @param PHP_CodeSniffer_File $phpcsFile The file being processed.
83     * @param int                  $stackPtr  The position where this token was
84     *                                        found.
85     * @param int                  $currScope The position of the current scope.
86     *
87     * @return void
88     */
89    protected function processTokenWithinScope(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $currScope)
90    {
91        $methodName = $phpcsFile->getDeclarationName($stackPtr);
92        if ($methodName === null) {
93            // Ignore closures.
94            return;
95        }
96
97        $className = $phpcsFile->getDeclarationName($currScope);
98        $errorData = array($className.'::'.$methodName);
99
100        // Is this a magic method. i.e., is prefixed with "__" ?
101        if (preg_match('|^__[^_]|', $methodName) !== 0) {
102            $magicPart = strtolower(substr($methodName, 2));
103            if (isset($this->magicMethods[$magicPart]) === false) {
104                 $error = 'Method name "%s" is invalid; only PHP magic methods should be prefixed with a double underscore';
105                 $phpcsFile->addError($error, $stackPtr, 'MethodDoubleUnderscore', $errorData);
106            }
107
108            return;
109        }
110
111        // PHP4 constructors are allowed to break our rules.
112        if ($methodName === $className) {
113            return;
114        }
115
116        // PHP4 destructors are allowed to break our rules.
117        if ($methodName === '_'.$className) {
118            return;
119        }
120
121        $methodProps    = $phpcsFile->getMethodProperties($stackPtr);
122        $scope          = $methodProps['scope'];
123        $scopeSpecified = $methodProps['scope_specified'];
124
125        if ($methodProps['scope'] === 'private') {
126            $isPublic = false;
127        } else {
128            $isPublic = true;
129        }
130
131        // If it's a private method, it must have an underscore on the front.
132        if ($isPublic === false) {
133            if ($methodName{0} !== '_') {
134                $error = 'Private method name "%s" must be prefixed with an underscore';
135                $phpcsFile->addError($error, $stackPtr, 'PrivateNoUnderscore', $errorData);
136                $phpcsFile->recordMetric($stackPtr, 'Private method prefixed with underscore', 'no');
137                return;
138            } else {
139                $phpcsFile->recordMetric($stackPtr, 'Private method prefixed with underscore', 'yes');
140            }
141        }
142
143        // If it's not a private method, it must not have an underscore on the front.
144        if ($isPublic === true && $scopeSpecified === true && $methodName{0} === '_') {
145            $error = '%s method name "%s" must not be prefixed with an underscore';
146            $data  = array(
147                      ucfirst($scope),
148                      $errorData[0],
149                     );
150            $phpcsFile->addError($error, $stackPtr, 'PublicUnderscore', $data);
151            return;
152        }
153
154        // If the scope was specified on the method, then the method must be
155        // camel caps and an underscore should be checked for. If it wasn't
156        // specified, treat it like a public method and remove the underscore
157        // prefix if there is one because we cant determine if it is private or
158        // public.
159        $testMethodName = $methodName;
160        if ($scopeSpecified === false && $methodName{0} === '_') {
161            $testMethodName = substr($methodName, 1);
162        }
163
164        if (PHP_CodeSniffer::isCamelCaps($testMethodName, false, $isPublic, false) === false) {
165            if ($scopeSpecified === true) {
166                $error = '%s method name "%s" is not in camel caps format';
167                $data  = array(
168                          ucfirst($scope),
169                          $errorData[0],
170                         );
171                $phpcsFile->addError($error, $stackPtr, 'ScopeNotCamelCaps', $data);
172            } else {
173                $error = 'Method name "%s" is not in camel caps format';
174                $phpcsFile->addError($error, $stackPtr, 'NotCamelCaps', $errorData);
175            }
176
177            return;
178        }
179
180    }//end processTokenWithinScope()
181
182
183    /**
184     * Processes the tokens outside the scope.
185     *
186     * @param PHP_CodeSniffer_File $phpcsFile The file being processed.
187     * @param int                  $stackPtr  The position where this token was
188     *                                        found.
189     *
190     * @return void
191     */
192    protected function processTokenOutsideScope(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
193    {
194        $functionName = $phpcsFile->getDeclarationName($stackPtr);
195        if ($functionName === null) {
196            // Ignore closures.
197            return;
198        }
199
200        if (ltrim($functionName, '_') === '') {
201            // Ignore special functions.
202            return;
203        }
204
205        $errorData = array($functionName);
206
207        // Is this a magic function. i.e., it is prefixed with "__".
208        if (preg_match('|^__[^_]|', $functionName) !== 0) {
209            $magicPart = strtolower(substr($functionName, 2));
210            if (isset($this->magicFunctions[$magicPart]) === false) {
211                 $error = 'Function name "%s" is invalid; only PHP magic methods should be prefixed with a double underscore';
212                 $phpcsFile->addError($error, $stackPtr, 'FunctionDoubleUnderscore', $errorData);
213            }
214
215            return;
216        }
217
218        // Function names can be in two parts; the package name and
219        // the function name.
220        $packagePart   = '';
221        $camelCapsPart = '';
222        $underscorePos = strrpos($functionName, '_');
223        if ($underscorePos === false) {
224            $camelCapsPart = $functionName;
225        } else {
226            $packagePart   = substr($functionName, 0, $underscorePos);
227            $camelCapsPart = substr($functionName, ($underscorePos + 1));
228
229            // We don't care about _'s on the front.
230            $packagePart = ltrim($packagePart, '_');
231        }
232
233        // If it has a package part, make sure the first letter is a capital.
234        if ($packagePart !== '') {
235            if ($functionName{0} === '_') {
236                $error = 'Function name "%s" is invalid; only private methods should be prefixed with an underscore';
237                $phpcsFile->addError($error, $stackPtr, 'FunctionUnderscore', $errorData);
238                return;
239            }
240
241            if ($functionName{0} !== strtoupper($functionName{0})) {
242                $error = 'Function name "%s" is prefixed with a package name but does not begin with a capital letter';
243                $phpcsFile->addError($error, $stackPtr, 'FunctionNoCapital', $errorData);
244                return;
245            }
246        }
247
248        // If it doesn't have a camel caps part, it's not valid.
249        if (trim($camelCapsPart) === '') {
250            $error = 'Function name "%s" is not valid; name appears incomplete';
251            $phpcsFile->addError($error, $stackPtr, 'FunctionInvalid', $errorData);
252            return;
253        }
254
255        $validName        = true;
256        $newPackagePart   = $packagePart;
257        $newCamelCapsPart = $camelCapsPart;
258
259        // Every function must have a camel caps part, so check that first.
260        if (PHP_CodeSniffer::isCamelCaps($camelCapsPart, false, true, false) === false) {
261            $validName        = false;
262            $newCamelCapsPart = strtolower($camelCapsPart{0}).substr($camelCapsPart, 1);
263        }
264
265        if ($packagePart !== '') {
266            // Check that each new word starts with a capital.
267            $nameBits = explode('_', $packagePart);
268            foreach ($nameBits as $bit) {
269                if ($bit{0} !== strtoupper($bit{0})) {
270                    $newPackagePart = '';
271                    foreach ($nameBits as $bit) {
272                        $newPackagePart .= strtoupper($bit{0}).substr($bit, 1).'_';
273                    }
274
275                    $validName = false;
276                    break;
277                }
278            }
279        }
280
281        if ($validName === false) {
282            $newName = rtrim($newPackagePart, '_').'_'.$newCamelCapsPart;
283            if ($newPackagePart === '') {
284                $newName = $newCamelCapsPart;
285            } else {
286                $newName = rtrim($newPackagePart, '_').'_'.$newCamelCapsPart;
287            }
288
289            $error  = 'Function name "%s" is invalid; consider "%s" instead';
290            $data   = $errorData;
291            $data[] = $newName;
292            $phpcsFile->addError($error, $stackPtr, 'FunctionNameInvalid', $data);
293        }
294
295    }//end processTokenOutsideScope()
296
297
298}//end class
299