• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

PHPCSStandards / PHPCSExtra / 15639493787

13 Jun 2025 04:35PM UTC coverage: 97.39% (-2.5%) from 99.85%
15639493787

Pull #367

github

web-flow
Merge 844746ea8 into 10343591c
Pull Request #367: Update for compatibility with PHPCS 4.0

55 of 140 new or added lines in 8 files covered. (39.29%)

1 existing line in 1 file now uncovered.

3358 of 3448 relevant lines covered (97.39%)

3.58 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.46
/Modernize/Sniffs/FunctionCalls/DirnameSniff.php
1
<?php
2
/**
3
 * PHPCSExtra, a collection of sniffs and standards for use with PHP_CodeSniffer.
4
 *
5
 * @package   PHPCSExtra
6
 * @copyright 2020 PHPCSExtra Contributors
7
 * @license   https://opensource.org/licenses/LGPL-3.0 LGPL3
8
 * @link      https://github.com/PHPCSStandards/PHPCSExtra
9
 */
10

11
namespace PHPCSExtra\Modernize\Sniffs\FunctionCalls;
12

13
use PHP_CodeSniffer\Files\File;
14
use PHP_CodeSniffer\Sniffs\Sniff;
15
use PHP_CodeSniffer\Util\Tokens;
16
use PHPCSUtils\BackCompat\Helper;
17
use PHPCSUtils\Tokens\Collections;
18
use PHPCSUtils\Utils\Context;
19
use PHPCSUtils\Utils\PassedParameters;
20

21
/**
22
 * Detect `dirname(__FILE__)` and nested uses of `dirname()`.
23
 *
24
 * @since 1.0.0
25
 */
26
final class DirnameSniff implements Sniff
27
{
28

29
    /**
30
     * PHP version as configured or 0 if unknown.
31
     *
32
     * @since 1.1.1
33
     *
34
     * @var int
35
     */
36
    private $phpVersion;
37

38
    /**
39
     * Registers the tokens that this sniff wants to listen for.
40
     *
41
     * @since 1.0.0
42
     *
43
     * @return array<int|string>
44
     */
45
    public function register()
4✔
46
    {
47
        return [
2✔
48
            \T_STRING,
4✔
49
            \T_NAME_FULLY_QUALIFIED,
4✔
50
        ];
4✔
51
    }
52

53
    /**
54
     * Processes this test, when one of its tokens is encountered.
55
     *
56
     * @since 1.0.0
57
     *
58
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
59
     * @param int                         $stackPtr  The position of the current token
60
     *                                               in the stack passed in $tokens.
61
     *
62
     * @return void
63
     */
64
    public function process(File $phpcsFile, $stackPtr)
4✔
65
    {
66
        if (isset($this->phpVersion) === false || \defined('PHP_CODESNIFFER_IN_TESTS')) {
4✔
67
            // Set default value to prevent this code from running every time the sniff is triggered.
68
            $this->phpVersion = 0;
4✔
69

70
            $phpVersion = Helper::getConfigData('php_version');
4✔
71
            if ($phpVersion !== null) {
4✔
72
                $this->phpVersion = (int) $phpVersion;
4✔
73
            }
2✔
74
        }
2✔
75

76
        if ($this->phpVersion !== 0 && $this->phpVersion < 50300) {
4✔
77
            // PHP version too low, nothing to do.
78
            return;
4✔
79
        }
80

81
        $tokens   = $phpcsFile->getTokens();
4✔
82
        $contents = $tokens[$stackPtr]['content'];
4✔
83
        if ($tokens[$stackPtr]['code'] === \T_NAME_FULLY_QUALIFIED) {
4✔
NEW
84
            $contents = \ltrim($contents, '\\');
×
85
        }
86

87
        if (\strtolower($contents) !== 'dirname') {
4✔
88
            // Not our target.
89
            return;
4✔
90
        }
91

92
        $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
4✔
93
        if ($nextNonEmpty === false
2✔
94
            || $tokens[$nextNonEmpty]['code'] !== \T_OPEN_PARENTHESIS
4✔
95
            || isset($tokens[$nextNonEmpty]['parenthesis_owner']) === true
4✔
96
        ) {
2✔
97
            // Not our target.
98
            return;
4✔
99
        }
100

101
        if (isset($tokens[$nextNonEmpty]['parenthesis_closer']) === false) {
4✔
102
            // Live coding or parse error, ignore.
103
            return;
4✔
104
        }
105

106
        if (Context::inAttribute($phpcsFile, $stackPtr) === true) {
4✔
107
            // Class instantiation in attribute, not function call.
108
            return;
4✔
109
        }
110

111
        // Check if it is really a function call to the global function.
112
        $prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
4✔
113

114
        if (isset(Collections::objectOperators()[$tokens[$prevNonEmpty]['code']]) === true
4✔
115
            || $tokens[$prevNonEmpty]['code'] === \T_NEW
4✔
116
        ) {
2✔
117
            // Method call, class instantiation or other "not our target".
118
            return;
4✔
119
        }
120

121
        if ($tokens[$prevNonEmpty]['code'] === \T_NS_SEPARATOR) {
4✔
122
            $prevPrevToken = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prevNonEmpty - 1), null, true);
4✔
123
            if ($tokens[$prevPrevToken]['code'] === \T_STRING
4✔
124
                || $tokens[$prevPrevToken]['code'] === \T_NAMESPACE
4✔
125
            ) {
2✔
126
                // Namespaced function.
127
                return;
4✔
128
            }
129
        }
2✔
130

131
        /*
132
         * As of here, we can be pretty sure this is a function call to the global function.
133
         */
134
        $opener = $nextNonEmpty;
4✔
135
        $closer = $tokens[$nextNonEmpty]['parenthesis_closer'];
4✔
136

137
        $parameters = PassedParameters::getParameters($phpcsFile, $stackPtr);
4✔
138
        $paramCount = \count($parameters);
4✔
139
        if (empty($parameters) || $paramCount > 2) {
4✔
140
            // No parameters or too many parameter.
141
            return;
4✔
142
        }
143

144
        $pathParam = PassedParameters::getParameterFromStack($parameters, 1, 'path');
4✔
145
        if ($pathParam === false) {
4✔
146
            // If the path parameter doesn't exist, there's nothing to do.
147
            return;
4✔
148
        }
149

150
        $levelsParam = PassedParameters::getParameterFromStack($parameters, 2, 'levels');
4✔
151
        if ($levelsParam === false && $paramCount === 2) {
4✔
152
            // There must be a typo in the param name or an otherwise stray parameter. Ignore.
153
            return;
4✔
154
        }
155

156
        /*
157
         * PHP 5.3+: Detect use of dirname(__FILE__).
158
         */
159
        if (\strtoupper($pathParam['clean']) === '__FILE__') {
4✔
160
            $levelsValue = false;
4✔
161

162
            // Determine if the issue is auto-fixable.
163
            $hasComment = $phpcsFile->findNext(Tokens::$commentTokens, ($opener + 1), $closer);
4✔
164
            $fixable    = ($hasComment === false);
4✔
165

166
            if ($fixable === true) {
4✔
167
                $levelsValue = $this->getLevelsValue($phpcsFile, $levelsParam);
4✔
168
                if ($levelsParam !== false && $levelsValue === false) {
4✔
169
                    // Can't autofix if we don't know the value of the $levels parameter.
170
                    $fixable = false;
4✔
171
                }
2✔
172
            }
2✔
173

174
            $error = 'Use the __DIR__ constant instead of calling dirname(__FILE__) (PHP >= 5.3)';
4✔
175
            $code  = 'FileConstant';
4✔
176

177
            // Throw the error.
178
            if ($fixable === false) {
4✔
179
                $phpcsFile->addError($error, $stackPtr, $code);
4✔
180
                return;
4✔
181
            }
182

183
            $fix = $phpcsFile->addFixableError($error, $stackPtr, $code);
4✔
184
            if ($fix === true) {
4✔
185
                if ($levelsParam === false || $levelsValue === 1) {
4✔
186
                    // No $levels or $levels set to 1: we can replace the complete function call.
187
                    $phpcsFile->fixer->beginChangeset();
4✔
188

189
                    $phpcsFile->fixer->replaceToken($stackPtr, '__DIR__');
4✔
190

191
                    for ($i = ($stackPtr + 1); $i <= $closer; $i++) {
4✔
192
                        $phpcsFile->fixer->replaceToken($i, '');
4✔
193
                    }
2✔
194

195
                    // Remove potential leading \.
196
                    if ($tokens[$prevNonEmpty]['code'] === \T_NS_SEPARATOR) {
4✔
197
                        $phpcsFile->fixer->replaceToken($prevNonEmpty, '');
4✔
198
                    }
2✔
199

200
                    $phpcsFile->fixer->endChangeset();
4✔
201
                } else {
2✔
202
                    // We can replace the $path parameter and will need to adjust the $levels parameter.
203
                    $filePtr   = $phpcsFile->findNext(\T_FILE, $pathParam['start'], ($pathParam['end'] + 1));
4✔
204
                    $levelsPtr = $phpcsFile->findNext(\T_LNUMBER, $levelsParam['start'], ($levelsParam['end'] + 1));
4✔
205

206
                    $phpcsFile->fixer->beginChangeset();
4✔
207
                    $phpcsFile->fixer->replaceToken($filePtr, '__DIR__');
4✔
208
                    $phpcsFile->fixer->replaceToken($levelsPtr, ($levelsValue - 1));
4✔
209
                    $phpcsFile->fixer->endChangeset();
4✔
210
                }
211
            }
2✔
212

213
            return;
4✔
214
        }
215

216
        /*
217
         * PHP 7.0+: Detect use of nested calls to dirname().
218
         */
219
        if ($this->phpVersion !== 0 && $this->phpVersion < 70000) {
4✔
220
            // No need to check for this issue if the PHP version would not allow for it anyway.
221
            return;
4✔
222
        }
223

224
        if (\preg_match('`^\s*\\\\?dirname\s*\(`i', $pathParam['clean']) !== 1) {
4✔
225
            return;
4✔
226
        }
227

228
        /*
229
         * Check if there is something _behind_ the nested dirname() call within the same parameter.
230
         *
231
         * Note: the findNext() calls are safe and will always match the dirname() function call
232
         * as otherwise the above regex wouldn't have matched.
233
         */
234
        $innerDirnamePtr = $phpcsFile->findNext($this->register(), $pathParam['start'], ($pathParam['end'] + 1));
4✔
235
        $innerOpener     = $phpcsFile->findNext(\T_OPEN_PARENTHESIS, ($innerDirnamePtr + 1), ($pathParam['end'] + 1));
4✔
236
        if (isset($tokens[$innerOpener]['parenthesis_closer']) === false) {
4✔
237
            // Shouldn't be possible.
238
            return; // @codeCoverageIgnore
239
        }
240

241
        $innerCloser = $tokens[$innerOpener]['parenthesis_closer'];
4✔
242
        if ($innerCloser !== $pathParam['end']) {
4✔
243
            $hasContentAfter = $phpcsFile->findNext(
4✔
244
                Tokens::$emptyTokens,
4✔
245
                ($innerCloser + 1),
4✔
246
                ($pathParam['end'] + 1),
4✔
247
                true
2✔
248
            );
4✔
249
            if ($hasContentAfter !== false) {
4✔
250
                // Matched code like: `dirname(dirname($file) . 'something')`. Ignore.
251
                return;
4✔
252
            }
253
        }
2✔
254

255
        /*
256
         * Determine if this is an auto-fixable error.
257
         */
258

259
        // Step 1: Are there comments ? If so, not auto-fixable as we don't want to remove comments.
260
        $fixable          = true;
4✔
261
        $outerLevelsValue = false;
4✔
262
        $innerParameters  = [];
4✔
263
        $innerLevelsParam = false;
4✔
264
        $innerLevelsValue = false;
4✔
265

266
        for ($i = ($opener + 1); $i < $closer; $i++) {
4✔
267
            if (isset(Tokens::$commentTokens[$tokens[$i]['code']])) {
4✔
268
                $fixable = false;
4✔
269
                break;
4✔
270
            }
271

272
            if ($tokens[$i]['code'] === \T_OPEN_PARENTHESIS
4✔
273
                && isset($tokens[$i]['parenthesis_closer'])
4✔
274
            ) {
2✔
275
                // Skip over everything within the nested dirname() function call.
276
                $i = $tokens[$i]['parenthesis_closer'];
4✔
277
            }
2✔
278
        }
2✔
279

280
        // Step 2: Does the `$levels` parameter exist for the outer dirname() call and if so, is it usable ?
281
        if ($fixable === true) {
4✔
282
            $outerLevelsValue = $this->getLevelsValue($phpcsFile, $levelsParam);
4✔
283
            if ($levelsParam !== false && $outerLevelsValue === false) {
4✔
284
                // Can't autofix if we don't know the value of the $levels parameter.
285
                $fixable = false;
4✔
286
            }
2✔
287
        }
2✔
288

289
        // Step 3: Does the `$levels` parameter exist for the inner dirname() call and if so, is it usable ?
290
        if ($fixable === true) {
4✔
291
            $innerParameters  = PassedParameters::getParameters($phpcsFile, $innerDirnamePtr);
4✔
292
            $innerLevelsParam = PassedParameters::getParameterFromStack($innerParameters, 2, 'levels');
4✔
293
            $innerLevelsValue = $this->getLevelsValue($phpcsFile, $innerLevelsParam);
4✔
294
            if ($innerLevelsParam !== false && $innerLevelsValue === false) {
4✔
295
                // Can't autofix if we don't know the value of the $levels parameter.
296
                $fixable = false;
4✔
297
            }
2✔
298
        }
2✔
299

300
        /*
301
         * Throw the error.
302
         */
303
        $error  = 'Pass the $levels parameter to the dirname() call instead of using nested dirname() calls';
4✔
304
        $error .= ' (PHP >= 7.0)';
4✔
305
        $code   = 'Nested';
4✔
306

307
        if ($fixable === false) {
4✔
308
            $phpcsFile->addError($error, $stackPtr, $code);
4✔
309
            return;
4✔
310
        }
311

312
        $fix = $phpcsFile->addFixableError($error, $stackPtr, $code);
4✔
313
        if ($fix === false) {
4✔
314
            return;
4✔
315
        }
316

317
        /*
318
         * Fix the error.
319
         */
320
        $phpcsFile->fixer->beginChangeset();
4✔
321

322
        // Remove the info in the _outer_ param call.
323
        for ($i = $opener; $i < $innerOpener; $i++) {
4✔
324
            $phpcsFile->fixer->replaceToken($i, '');
4✔
325
        }
2✔
326

327
        for ($i = ($innerCloser + 1); $i <= $closer; $i++) {
4✔
328
            $phpcsFile->fixer->replaceToken($i, '');
4✔
329
        }
2✔
330

331
        if ($innerLevelsParam !== false) {
4✔
332
            // Inner $levels parameter already exists, just adjust the value.
333
            $innerLevelsPtr = $phpcsFile->findNext(
4✔
334
                \T_LNUMBER,
4✔
335
                $innerLevelsParam['start'],
4✔
336
                ($innerLevelsParam['end'] + 1)
4✔
337
            );
4✔
338
            $phpcsFile->fixer->replaceToken($innerLevelsPtr, ($innerLevelsValue + $outerLevelsValue));
4✔
339
        } else {
2✔
340
            // Inner $levels parameter does not exist yet. We need to add it.
341
            $content = ', ';
4✔
342

343
            $prevBeforeCloser = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($innerCloser - 1), null, true);
4✔
344
            if ($tokens[$prevBeforeCloser]['code'] === \T_COMMA) {
4✔
345
                // Trailing comma found, no need to add the comma.
346
                $content = ' ';
4✔
347
            }
2✔
348

349
            $innerPathParam = PassedParameters::getParameterFromStack($innerParameters, 1, 'path');
4✔
350
            if (isset($innerPathParam['name_token']) === true) {
4✔
351
                // Non-named param cannot follow named param, so add param name.
352
                $content .= 'levels: ';
4✔
353
            }
2✔
354

355
            $content .= ($innerLevelsValue + $outerLevelsValue);
4✔
356
            $phpcsFile->fixer->addContentBefore($innerCloser, $content);
4✔
357
        }
358

359
        $phpcsFile->fixer->endChangeset();
4✔
360
    }
2✔
361

362
    /**
363
     * Determine the value of the $levels parameter passed to dirname().
364
     *
365
     * @since 1.0.0
366
     *
367
     * @param \PHP_CodeSniffer\Files\File     $phpcsFile   The file being scanned.
368
     * @param array<string, int|string>|false $levelsParam The information about the parameter as retrieved
369
     *                                                     via PassedParameters::getParameterFromStack().
370
     *
371
     * @return int|false Integer levels value or FALSE if the levels value couldn't be determined.
372
     */
373
    private function getLevelsValue($phpcsFile, $levelsParam)
4✔
374
    {
375
        if ($levelsParam === false) {
4✔
376
            return 1;
4✔
377
        }
378

379
        $ignore   = Tokens::$emptyTokens;
4✔
380
        $ignore[] = \T_LNUMBER;
4✔
381

382
        $hasNonNumber = $phpcsFile->findNext($ignore, $levelsParam['start'], ($levelsParam['end'] + 1), true);
4✔
383
        if ($hasNonNumber !== false) {
4✔
384
            return false;
4✔
385
        }
386

387
        return (int) $levelsParam['clean'];
4✔
388
    }
389
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc