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

keradus / PHP-CS-Fixer / 17319949156

29 Aug 2025 09:20AM UTC coverage: 94.696% (-0.05%) from 94.744%
17319949156

push

github

keradus
CS

28333 of 29920 relevant lines covered (94.7%)

45.63 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
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
51
 */
52
final class TrailingCommaInMultilineFixer extends AbstractFixer implements ConfigurableFixerInterface
53
{
54
    /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
55
    use ConfigurableFixerTrait;
56

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

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

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

72
    private const MATCH_EXPRESSIONS = 'match';
73

74
    private const ARRAY_DESTRUCTURING = 'array_destructuring';
75

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

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

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

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

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

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

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

154
                continue;
1✔
155
            }
156

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

162
                continue;
24✔
163
            }
164

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

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

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

176
                continue;
32✔
177
            }
178

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

184
                continue;
2✔
185
            }
186

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

190
                continue;
3✔
191
            }
192

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

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

201
                continue;
13✔
202
            }
203

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

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

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

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

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

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

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

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