ArrayDoubleArrowAlignmentSniff::getPreviousComma()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 22
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 11
c 1
b 0
f 0
nc 4
nop 3
dl 0
loc 22
rs 9.9
1
<?php
2
3
/**
4
 * This file is part of the mo4-coding-standard (phpcs standard)
5
 *
6
 * @author  Xaver Loppenstedt <[email protected]>
7
 *
8
 * @license http://spdx.org/licenses/MIT MIT License
9
 *
10
 * @link    https://github.com/mayflower/mo4-coding-standard
11
 */
12
13
declare(strict_types=1);
14
15
namespace MO4\Sniffs\Arrays;
16
17
use PHP_CodeSniffer\Files\File;
18
use PHP_CodeSniffer\Sniffs\Sniff;
19
use PHP_CodeSniffer\Util\Tokens as PHP_CodeSniffer_Tokens;
20
21
/**
22
 * Array Double Arrow Alignment sniff.
23
 *
24
 * '=>' must be aligned in arrays, and the key and the '=>' must be in the same line
25
 *
26
 * @author    Xaver Loppenstedt <[email protected]>
27
 *
28
 * @copyright 2013 Xaver Loppenstedt, some rights reserved.
29
 *
30
 * @license   http://spdx.org/licenses/MIT MIT License
31
 *
32
 * @link      https://github.com/mayflower/mo4-coding-standard
33
 */
34
class ArrayDoubleArrowAlignmentSniff implements Sniff
35
{
36
    /**
37
     * Define all types of arrays.
38
     *
39
     * @var array
40
     */
41
    protected $arrayTokens = [
42
        // @phan-suppress-next-line PhanUndeclaredConstant
43
        T_OPEN_SHORT_ARRAY,
44
        T_ARRAY,
45
    ];
46
47
    /**
48
     * Registers the tokens that this sniff wants to listen for.
49
     *
50
     * @return array<int, int>
51
     *
52
     * @see    Tokens.php
53
     */
54
    public function register(): array
55
    {
56
        return $this->arrayTokens;
57
    }
58
59
    /**
60
     * Processes this test, when one of its tokens is encountered.
61
     *
62
     * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint
63
     *
64
     * @param File $phpcsFile The file being scanned.
65
     * @param int  $stackPtr  The position of the current token in
66
     *                        the stack passed in $tokens.
67
     *
68
     * @return void
69
     */
70
    public function process(File $phpcsFile, $stackPtr): void
71
    {
72
        $tokens  = $phpcsFile->getTokens();
73
        $current = $tokens[$stackPtr];
74
75
        if (T_ARRAY === $current['code']) {
76
            $start = $current['parenthesis_opener'];
77
            $end   = $current['parenthesis_closer'];
78
        } else {
79
            $start = $current['bracket_opener'];
80
            $end   = $current['bracket_closer'];
81
        }
82
83
        if ($tokens[$start]['line'] === $tokens[$end]['line']) {
84
            return;
85
        }
86
87
        // phpcs:disable
88
        /** @var array<int> $assignments */
89
        $assignments  = [];
90
        // phpcs:enable
91
        $keyEndColumn = -1;
92
        $lastLine     = -1;
93
94
        for ($i = ($start + 1); $i < $end; $i++) {
95
            $current  = $tokens[$i];
96
            $previous = $tokens[($i - 1)];
97
98
            // Skip nested arrays.
99
            if (true === \in_array($current['code'], $this->arrayTokens, true)) {
100
                $i = T_ARRAY === $current['code'] ? ($current['parenthesis_closer'] + 1) : ($current['bracket_closer'] + 1);
101
102
                continue;
103
            }
104
105
            // Skip closures in array.
106
            if (T_CLOSURE === $current['code']) {
107
                $i = ($current['scope_closer'] + 1);
108
109
                continue;
110
            }
111
112
            $i = (int) $i;
113
114
            if (T_DOUBLE_ARROW !== $current['code']) {
115
                continue;
116
            }
117
118
            $assignments[] = $i;
119
            $column        = $previous['column'];
120
            $line          = $current['line'];
121
122
            if ($lastLine === $line) {
123
                $previousComma = $this->getPreviousComma($phpcsFile, $i, $start);
124
125
                $msg = 'only one "=>" assignments per line is allowed in a multi line array';
126
127
                if (false !== $previousComma) {
128
                    $fixable = $phpcsFile->addFixableError($msg, $i, 'OneAssignmentPerLine');
129
130
                    if (true === $fixable) {
131
                        $phpcsFile->fixer->beginChangeset();
132
                        $phpcsFile->fixer->addNewline((int) $previousComma);
133
                        $phpcsFile->fixer->endChangeset();
134
                    }
135
                } else {
136
                    // Remove current and previous '=>' from array for further processing.
137
                    \array_pop($assignments);
138
                    \array_pop($assignments);
139
                    $phpcsFile->addError($msg, $i, 'OneAssignmentPerLine');
140
                }
141
            }
142
143
            $hasKeyInLine = false;
144
145
            $j = ($i - 1);
146
147
            while (($j >= 0) && ($tokens[$j]['line'] === $current['line'])) {
148
                if (false === \in_array($tokens[$j]['code'], PHP_CodeSniffer_Tokens::$emptyTokens, true)) {
149
                    $hasKeyInLine = true;
150
                }
151
152
                $j--;
153
            }
154
155
            if (false === $hasKeyInLine) {
156
                $fixable = $phpcsFile->addFixableError(
157
                    'in arrays, keys and "=>" must be on the same line',
158
                    $i,
159
                    'KeyAndValueNotOnSameLine'
160
                );
161
162
                if (true === $fixable) {
163
                    $phpcsFile->fixer->beginChangeset();
164
                    $phpcsFile->fixer->replaceToken($j, '');
165
                    $phpcsFile->fixer->endChangeset();
166
                }
167
            }
168
169
            if ($column > $keyEndColumn) {
170
                $keyEndColumn = $column;
171
            }
172
173
            $lastLine = $line;
174
        }
175
176
        $doubleArrowStartColumn = ($keyEndColumn + 1);
177
178
        foreach ($assignments as $ptr) {
179
            $current = $tokens[$ptr];
180
            $column  = $current['column'];
181
182
            $beforeArrowPtr = ($ptr - 1);
183
            $currentIndent  = \strlen($tokens[$beforeArrowPtr]['content']);
184
            $correctIndent  = ($currentIndent - $column + $doubleArrowStartColumn);
185
186
            if ($column === $doubleArrowStartColumn) {
187
                continue;
188
            }
189
190
            $fixable = $phpcsFile->addFixableError("each \"=>\" assignments must be aligned; current indentation before \"=>\" are {$currentIndent} space(s), must be {$correctIndent} space(s)", $ptr, 'AssignmentsNotAligned');
191
192
            if (false === $fixable) {
193
                continue;
194
            }
195
196
            $phpcsFile->fixer->beginChangeset();
197
198
            if (T_WHITESPACE === $tokens[$beforeArrowPtr]['code']) {
199
                $phpcsFile->fixer->replaceToken($beforeArrowPtr, \str_repeat(' ', $correctIndent));
200
            } else {
201
                $phpcsFile->fixer->addContent($beforeArrowPtr, \str_repeat(' ', $correctIndent));
202
            }
203
204
            $phpcsFile->fixer->endChangeset();
205
        }
206
    }
207
208
    /**
209
     * Find previous comma in array.
210
     *
211
     * @param File $phpcsFile The file being scanned.
212
     * @param int  $stackPtr  The position of the current token in
213
     *                        the stack passed in $tokens.
214
     * @param int  $start     Start of the array
215
     *
216
     * @return bool|int
217
     */
218
    private function getPreviousComma(File $phpcsFile, int $stackPtr, int $start)
219
    {
220
        $previousComma = false;
221
        $tokens        = $phpcsFile->getTokens();
222
223
        $ptr = $phpcsFile->findPrevious([T_COMMA, T_CLOSE_SHORT_ARRAY], $stackPtr, $start);
224
225
        while (false !== $ptr) {
226
            if (T_COMMA === $tokens[$ptr]['code']) {
227
                $previousComma = $ptr;
228
229
                break;
230
            }
231
232
            if (T_CLOSE_SHORT_ARRAY === $tokens[$ptr]['code']) {
233
                $ptr = $tokens[$ptr]['bracket_opener'];
234
            }
235
236
            $ptr = $phpcsFile->findPrevious([T_COMMA, T_CLOSE_SHORT_ARRAY], ($ptr - 1), $start);
237
        }
238
239
        return $previousComma;
240
    }
241
}
242