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

keradus / PHP-CS-Fixer / 15274850899

27 May 2025 11:01AM UTC coverage: 94.849% (-0.02%) from 94.87%
15274850899

push

github

web-flow
DX: introduce `FCT` class for tokens not present in the lowest supported PHP version (#8706)

Co-authored-by: Dariusz Rumiński <dariusz.ruminski@gmail.com>

186 of 192 new or added lines in 52 files covered. (96.88%)

72 existing lines in 9 files now uncovered.

28099 of 29625 relevant lines covered (94.85%)

45.33 hits per line

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

98.89
/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
 * @author Sebastiaan Stok <s.stok@rollerscapes.net>
36
 * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
37
 * @author Kuba Werłos <werlos@gmail.com>
38
 *
39
 * @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
40
 *
41
 * @phpstan-type _AutogeneratedInputConfiguration array{
42
 *  after_heredoc?: bool,
43
 *  elements?: list<'arguments'|'array_destructuring'|'arrays'|'match'|'parameters'>,
44
 * }
45
 * @phpstan-type _AutogeneratedComputedConfiguration array{
46
 *  after_heredoc: bool,
47
 *  elements: list<'arguments'|'array_destructuring'|'arrays'|'match'|'parameters'>,
48
 * }
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
                    ,
3✔
92
                    ['after_heredoc' => true]
3✔
93
                ),
3✔
94
                new CodeSample("<?php\nfoo(\n    1,\n    2\n);\n", ['elements' => [self::ELEMENTS_ARGUMENTS]]),
3✔
95
                new VersionSpecificCodeSample("<?php\nfunction foo(\n    \$x,\n    \$y\n)\n{\n}\n", new VersionSpecification(8_00_00), ['elements' => [self::ELEMENTS_PARAMETERS]]),
3✔
96
            ]
3✔
97
        );
3✔
98
    }
99

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

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

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

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

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

153
                continue;
1✔
154
            }
155

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

161
                continue;
24✔
162
            }
163

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

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

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

175
                continue;
32✔
176
            }
177

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

183
                continue;
2✔
184
            }
185

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

189
                continue;
3✔
190
            }
191

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

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

200
                continue;
13✔
201
            }
202

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

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

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

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

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

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

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

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