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

keradus / PHP-CS-Fixer / 17013625939

15 Aug 2025 09:45PM UTC coverage: 94.74% (+0.01%) from 94.73%
17013625939

push

github

web-flow
chore: extract token types for PHPStan (#8925)

28260 of 29829 relevant lines covered (94.74%)

45.88 hits per line

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

98.88
/src/Fixer/ControlStructure/TrailingCommaInMultilineFixer.php
1
<?php
2

3
declare(strict_types=1);
4

5
/*
6
 * This file is part of PHP CS Fixer.
7
 *
8
 * (c) Fabien Potencier <fabien@symfony.com>
9
 *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
10
 *
11
 * This source file is subject to the MIT license that is bundled
12
 * with this source code in the file LICENSE.
13
 */
14

15
namespace PhpCsFixer\Fixer\ControlStructure;
16

17
use PhpCsFixer\AbstractFixer;
18
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
19
use PhpCsFixer\Fixer\ConfigurableFixerTrait;
20
use PhpCsFixer\FixerConfiguration\AllowedValueSubset;
21
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
22
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
23
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
24
use PhpCsFixer\FixerDefinition\CodeSample;
25
use PhpCsFixer\FixerDefinition\FixerDefinition;
26
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
27
use PhpCsFixer\FixerDefinition\VersionSpecification;
28
use PhpCsFixer\FixerDefinition\VersionSpecificCodeSample;
29
use PhpCsFixer\Tokenizer\CT;
30
use PhpCsFixer\Tokenizer\Token;
31
use PhpCsFixer\Tokenizer\Tokens;
32
use PhpCsFixer\Tokenizer\TokensAnalyzer;
33

34
/**
35
 * @phpstan-type _AutogeneratedInputConfiguration array{
36
 *  after_heredoc?: bool,
37
 *  elements?: list<'arguments'|'array_destructuring'|'arrays'|'match'|'parameters'>,
38
 * }
39
 * @phpstan-type _AutogeneratedComputedConfiguration array{
40
 *  after_heredoc: bool,
41
 *  elements: list<'arguments'|'array_destructuring'|'arrays'|'match'|'parameters'>,
42
 * }
43
 *
44
 * @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
45
 *
46
 * @author Sebastiaan Stok <s.stok@rollerscapes.net>
47
 * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
48
 * @author Kuba Werłos <werlos@gmail.com>
49
 */
50
final class TrailingCommaInMultilineFixer extends AbstractFixer implements ConfigurableFixerInterface
51
{
52
    /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
53
    use ConfigurableFixerTrait;
54

55
    /**
56
     * @internal
57
     */
58
    public const ELEMENTS_ARRAYS = 'arrays';
59

60
    /**
61
     * @internal
62
     */
63
    public const ELEMENTS_ARGUMENTS = 'arguments';
64

65
    /**
66
     * @internal
67
     */
68
    public const ELEMENTS_PARAMETERS = 'parameters';
69

70
    private const MATCH_EXPRESSIONS = 'match';
71

72
    private const ARRAY_DESTRUCTURING = 'array_destructuring';
73

74
    public function getDefinition(): FixerDefinitionInterface
75
    {
76
        return new FixerDefinition(
3✔
77
            'Arguments lists, array destructuring lists, arrays that are multi-line, `match`-lines and parameters lists must have a trailing comma.',
3✔
78
            [
3✔
79
                new CodeSample("<?php\narray(\n    1,\n    2\n);\n"),
3✔
80
                new CodeSample(
3✔
81
                    <<<'SAMPLE'
3✔
82
                        <?php
83
                            $x = [
84
                                'foo',
85
                                <<<EOD
86
                                    bar
87
                                    EOD
88
                            ];
89

90
                        SAMPLE,
3✔
91
                    ['after_heredoc' => true]
3✔
92
                ),
3✔
93
                new CodeSample("<?php\nfoo(\n    1,\n    2\n);\n", ['elements' => [self::ELEMENTS_ARGUMENTS]]),
3✔
94
                new VersionSpecificCodeSample("<?php\nfunction foo(\n    \$x,\n    \$y\n)\n{\n}\n", new VersionSpecification(8_00_00), ['elements' => [self::ELEMENTS_PARAMETERS]]),
3✔
95
            ]
3✔
96
        );
3✔
97
    }
98

99
    /**
100
     * {@inheritdoc}
101
     *
102
     * Must run after MultilinePromotedPropertiesFixer.
103
     */
104
    public function getPriority(): int
105
    {
106
        return 0;
1✔
107
    }
108

109
    public function isCandidate(Tokens $tokens): bool
110
    {
111
        return $tokens->isAnyTokenKindsFound([\T_ARRAY, CT::T_ARRAY_SQUARE_BRACE_OPEN, '(', CT::T_DESTRUCTURING_SQUARE_BRACE_OPEN]);
79✔
112
    }
113

114
    protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
115
    {
116
        return new FixerConfigurationResolver([
88✔
117
            (new FixerOptionBuilder('after_heredoc', 'Whether a trailing comma should also be placed after heredoc end.'))
88✔
118
                ->setAllowedTypes(['bool'])
88✔
119
                ->setDefault(false) // @TODO 4.0: set to true
88✔
120
                ->getOption(),
88✔
121
            (new FixerOptionBuilder('elements', \sprintf('Where to fix multiline trailing comma (PHP >= 8.0 for `%s` and `%s`).', self::ELEMENTS_PARAMETERS, self::MATCH_EXPRESSIONS))) // @TODO: remove text when PHP 8.0+ is required
88✔
122
                ->setAllowedTypes(['string[]'])
88✔
123
                ->setAllowedValues([
88✔
124
                    new AllowedValueSubset([
88✔
125
                        self::ARRAY_DESTRUCTURING,
88✔
126
                        self::ELEMENTS_ARGUMENTS,
88✔
127
                        self::ELEMENTS_ARRAYS,
88✔
128
                        self::ELEMENTS_PARAMETERS,
88✔
129
                        self::MATCH_EXPRESSIONS,
88✔
130
                    ]),
88✔
131
                ])
88✔
132
                ->setDefault([self::ELEMENTS_ARRAYS])
88✔
133
                ->getOption(),
88✔
134
        ]);
88✔
135
    }
136

137
    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
138
    {
139
        $configuredElements = $this->configuration['elements'];
79✔
140
        $fixArrays = \in_array(self::ELEMENTS_ARRAYS, $configuredElements, true);
79✔
141
        $fixArguments = \in_array(self::ELEMENTS_ARGUMENTS, $configuredElements, true);
79✔
142
        $fixParameters = \PHP_VERSION_ID >= 8_00_00 && \in_array(self::ELEMENTS_PARAMETERS, $configuredElements, true); // @TODO: drop condition when PHP 8.0+ is required
79✔
143
        $fixMatch = \PHP_VERSION_ID >= 8_00_00 && \in_array(self::MATCH_EXPRESSIONS, $configuredElements, true); // @TODO: drop condition when PHP 8.0+ is required
79✔
144
        $fixDestructuring = \in_array(self::ARRAY_DESTRUCTURING, $configuredElements, true);
79✔
145

146
        for ($index = $tokens->count() - 1; $index >= 0; --$index) {
79✔
147
            if ($tokens[$index]->isGivenKind(CT::T_DESTRUCTURING_SQUARE_BRACE_OPEN)) {
79✔
148
                if ($fixDestructuring) { // array destructing short syntax
1✔
149
                    $this->fixBlock($tokens, $index);
1✔
150
                }
151

152
                continue;
1✔
153
            }
154

155
            if ($tokens[$index]->isGivenKind(CT::T_ARRAY_SQUARE_BRACE_OPEN)) {
79✔
156
                if ($fixArrays) { // array short syntax
24✔
157
                    $this->fixBlock($tokens, $index);
21✔
158
                }
159

160
                continue;
24✔
161
            }
162

163
            if (!$tokens[$index]->equals('(')) {
79✔
164
                continue;
79✔
165
            }
166

167
            $prevIndex = $tokens->getPrevMeaningfulToken($index);
66✔
168

169
            if ($tokens[$prevIndex]->isGivenKind(\T_ARRAY)) {
66✔
170
                if ($fixArrays) { // array long syntax
32✔
171
                    $this->fixBlock($tokens, $index);
31✔
172
                }
173

174
                continue;
32✔
175
            }
176

177
            if ($tokens[$prevIndex]->isGivenKind(\T_LIST)) {
39✔
178
                if ($fixDestructuring || $fixArguments) { // array destructing long syntax
2✔
179
                    $this->fixBlock($tokens, $index);
2✔
180
                }
181

182
                continue;
2✔
183
            }
184

185
            if ($fixMatch && $tokens[$prevIndex]->isGivenKind(\T_MATCH)) {
38✔
186
                $this->fixBlock($tokens, $tokens->getNextTokenOfKind($index, ['{']));
3✔
187

188
                continue;
3✔
189
            }
190

191
            $prevPrevIndex = $tokens->getPrevMeaningfulToken($prevIndex);
37✔
192

193
            if ($fixArguments
37✔
194
                && $tokens[$prevIndex]->equalsAny([']', [\T_CLASS], [\T_STRING], [\T_VARIABLE], [\T_STATIC], [\T_ISSET], [\T_UNSET], [\T_LIST]])
37✔
195
                && !$tokens[$prevPrevIndex]->isGivenKind(\T_FUNCTION)
37✔
196
            ) {
197
                $this->fixBlock($tokens, $index);
13✔
198

199
                continue;
13✔
200
            }
201

202
            if (
203
                $fixParameters
26✔
204
                && (
205
                    $tokens[$prevIndex]->isGivenKind(\T_STRING)
26✔
206
                    && $tokens[$prevPrevIndex]->isGivenKind(\T_FUNCTION)
26✔
207
                    || $tokens[$prevIndex]->isGivenKind([\T_FN, \T_FUNCTION])
26✔
208
                )
209
            ) {
210
                $this->fixBlock($tokens, $index);
6✔
211
            }
212
        }
213
    }
214

215
    private function fixBlock(Tokens $tokens, int $startIndex): void
216
    {
217
        $tokensAnalyzer = new TokensAnalyzer($tokens);
68✔
218

219
        if (!$tokensAnalyzer->isBlockMultiline($tokens, $startIndex)) {
68✔
220
            return;
23✔
221
        }
222

223
        $blockType = Tokens::detectBlockType($tokens[$startIndex]);
49✔
224
        $endIndex = $tokens->findBlockEnd($blockType['type'], $startIndex);
49✔
225

226
        $beforeEndIndex = $tokens->getPrevMeaningfulToken($endIndex);
49✔
227
        if (!$tokens->isPartialCodeMultiline($beforeEndIndex, $endIndex)) {
49✔
228
            return;
9✔
229
        }
230
        $beforeEndToken = $tokens[$beforeEndIndex];
40✔
231

232
        // if there is some item between braces then add `,` after it
233
        if (
234
            $startIndex !== $beforeEndIndex && !$beforeEndToken->equals(',')
40✔
235
            && (true === $this->configuration['after_heredoc'] || !$beforeEndToken->isGivenKind(\T_END_HEREDOC))
40✔
236
        ) {
237
            $tokens->insertAt($beforeEndIndex + 1, new Token(','));
32✔
238

239
            $endToken = $tokens[$endIndex];
32✔
240

241
            if (!$endToken->isComment() && !$endToken->isWhitespace()) {
32✔
242
                $tokens->ensureWhitespaceAtIndex($endIndex, 1, ' ');
×
243
            }
244
        }
245
    }
246
}
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

© 2026 Coveralls, Inc