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

keradus / PHP-CS-Fixer / 18051010410

26 Sep 2025 10:40PM UTC coverage: 94.308% (-0.02%) from 94.331%
18051010410

push

github

web-flow
chore: use accidentally missing `@auto:risky` (#9102)

28583 of 30308 relevant lines covered (94.31%)

45.24 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\Future;
30
use PhpCsFixer\Tokenizer\CT;
31
use PhpCsFixer\Tokenizer\Token;
32
use PhpCsFixer\Tokenizer\Tokens;
33
use PhpCsFixer\Tokenizer\TokensAnalyzer;
34

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

58
    /**
59
     * @internal
60
     */
61
    public const ELEMENTS_ARRAYS = 'arrays';
62

63
    /**
64
     * @internal
65
     */
66
    public const ELEMENTS_ARGUMENTS = 'arguments';
67

68
    /**
69
     * @internal
70
     */
71
    public const ELEMENTS_PARAMETERS = 'parameters';
72

73
    private const MATCH_EXPRESSIONS = 'match';
74

75
    private const ARRAY_DESTRUCTURING = 'array_destructuring';
76

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

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

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

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

117
    protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
118
    {
119
        return new FixerConfigurationResolver([
88✔
120
            (new FixerOptionBuilder('after_heredoc', 'Whether a trailing comma should also be placed after heredoc end.'))
88✔
121
                ->setAllowedTypes(['bool'])
88✔
122
                ->setDefault(Future::getV4OrV3(true, false))
88✔
123
                ->getOption(),
88✔
124
            (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✔
125
                ->setAllowedTypes(['string[]'])
88✔
126
                ->setAllowedValues([
88✔
127
                    new AllowedValueSubset([
88✔
128
                        self::ARRAY_DESTRUCTURING,
88✔
129
                        self::ELEMENTS_ARGUMENTS,
88✔
130
                        self::ELEMENTS_ARRAYS,
88✔
131
                        self::ELEMENTS_PARAMETERS,
88✔
132
                        self::MATCH_EXPRESSIONS,
88✔
133
                    ]),
88✔
134
                ])
88✔
135
                ->setDefault([self::ELEMENTS_ARRAYS])
88✔
136
                ->getOption(),
88✔
137
        ]);
88✔
138
    }
139

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

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

155
                continue;
1✔
156
            }
157

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

163
                continue;
24✔
164
            }
165

166
            if (!$tokens[$index]->equals('(')) {
79✔
167
                continue;
79✔
168
            }
169

170
            $prevIndex = $tokens->getPrevMeaningfulToken($index);
66✔
171

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

177
                continue;
32✔
178
            }
179

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

185
                continue;
2✔
186
            }
187

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

191
                continue;
3✔
192
            }
193

194
            $prevPrevIndex = $tokens->getPrevMeaningfulToken($prevIndex);
37✔
195

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

202
                continue;
13✔
203
            }
204

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

218
    private function fixBlock(Tokens $tokens, int $startIndex): void
219
    {
220
        $tokensAnalyzer = new TokensAnalyzer($tokens);
68✔
221

222
        if (!$tokensAnalyzer->isBlockMultiline($tokens, $startIndex)) {
68✔
223
            return;
23✔
224
        }
225

226
        $blockType = Tokens::detectBlockType($tokens[$startIndex]);
49✔
227
        $endIndex = $tokens->findBlockEnd($blockType['type'], $startIndex);
49✔
228

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

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

242
            $endToken = $tokens[$endIndex];
32✔
243

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