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

keradus / PHP-CS-Fixer / 16018263876

02 Jul 2025 06:58AM UTC coverage: 94.846% (-0.002%) from 94.848%
16018263876

push

github

keradus
debug2

28193 of 29725 relevant lines covered (94.85%)

45.34 hits per line

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

97.52
/src/Fixer/Basic/BracesPositionFixer.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\Basic;
16

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

35
/**
36
 * @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
37
 *
38
 * @phpstan-type _AutogeneratedInputConfiguration array{
39
 *  allow_single_line_anonymous_functions?: bool,
40
 *  allow_single_line_empty_anonymous_classes?: bool,
41
 *  anonymous_classes_opening_brace?: 'next_line_unless_newline_at_signature_end'|'same_line',
42
 *  anonymous_functions_opening_brace?: 'next_line_unless_newline_at_signature_end'|'same_line',
43
 *  classes_opening_brace?: 'next_line_unless_newline_at_signature_end'|'same_line',
44
 *  control_structures_opening_brace?: 'next_line_unless_newline_at_signature_end'|'same_line',
45
 *  functions_opening_brace?: 'next_line_unless_newline_at_signature_end'|'same_line',
46
 * }
47
 * @phpstan-type _AutogeneratedComputedConfiguration array{
48
 *  allow_single_line_anonymous_functions: bool,
49
 *  allow_single_line_empty_anonymous_classes: bool,
50
 *  anonymous_classes_opening_brace: 'next_line_unless_newline_at_signature_end'|'same_line',
51
 *  anonymous_functions_opening_brace: 'next_line_unless_newline_at_signature_end'|'same_line',
52
 *  classes_opening_brace: 'next_line_unless_newline_at_signature_end'|'same_line',
53
 *  control_structures_opening_brace: 'next_line_unless_newline_at_signature_end'|'same_line',
54
 *  functions_opening_brace: 'next_line_unless_newline_at_signature_end'|'same_line',
55
 * }
56
 */
57
final class BracesPositionFixer extends AbstractFixer implements ConfigurableFixerInterface, WhitespacesAwareFixerInterface
58
{
59
    /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
60
    use ConfigurableFixerTrait;
61

62
    use Indentation;
63

64
    /**
65
     * @internal
66
     */
67
    public const NEXT_LINE_UNLESS_NEWLINE_AT_SIGNATURE_END = 'next_line_unless_newline_at_signature_end';
68

69
    /**
70
     * @internal
71
     */
72
    public const SAME_LINE = 'same_line';
73
    private const CONTROL_STRUCTURE_TOKENS = [T_DECLARE, T_DO, T_ELSE, T_ELSEIF, T_FINALLY, T_FOR, T_FOREACH, T_IF, T_WHILE, T_TRY, T_CATCH, T_SWITCH, FCT::T_MATCH];
74

75
    public function getDefinition(): FixerDefinitionInterface
76
    {
77
        return new FixerDefinition(
3✔
78
            'Braces must be placed as configured.',
3✔
79
            [
3✔
80
                new CodeSample(
3✔
81
                    '<?php
3✔
82
class Foo {
83
}
84

85
function foo() {
86
}
87

88
$foo = function()
89
{
90
};
91

92
if (foo())
93
{
94
    bar();
95
}
96

97
$foo = new class
98
{
99
};
100
'
3✔
101
                ),
3✔
102
                new CodeSample(
3✔
103
                    '<?php
3✔
104
if (foo()) {
105
    bar();
106
}
107
',
3✔
108
                    ['control_structures_opening_brace' => self::NEXT_LINE_UNLESS_NEWLINE_AT_SIGNATURE_END]
3✔
109
                ),
3✔
110
                new CodeSample(
3✔
111
                    '<?php
3✔
112
function foo()
113
{
114
}
115
',
3✔
116
                    ['functions_opening_brace' => self::SAME_LINE]
3✔
117
                ),
3✔
118
                new CodeSample(
3✔
119
                    '<?php
3✔
120
$foo = function () {
121
};
122
',
3✔
123
                    ['anonymous_functions_opening_brace' => self::NEXT_LINE_UNLESS_NEWLINE_AT_SIGNATURE_END]
3✔
124
                ),
3✔
125
                new CodeSample(
3✔
126
                    '<?php
3✔
127
class Foo
128
{
129
}
130
',
3✔
131
                    ['classes_opening_brace' => self::SAME_LINE]
3✔
132
                ),
3✔
133
                new CodeSample(
3✔
134
                    '<?php
3✔
135
$foo = new class {
136
};
137
',
3✔
138
                    ['anonymous_classes_opening_brace' => self::NEXT_LINE_UNLESS_NEWLINE_AT_SIGNATURE_END]
3✔
139
                ),
3✔
140
                new CodeSample(
3✔
141
                    '<?php
3✔
142
$foo = new class { };
143
$bar = new class { private $baz; };
144
',
3✔
145
                    ['allow_single_line_empty_anonymous_classes' => true]
3✔
146
                ),
3✔
147
                new CodeSample(
3✔
148
                    '<?php
3✔
149
$foo = function () { return true; };
150
$bar = function () { $result = true;
151
    return $result; };
152
',
3✔
153
                    ['allow_single_line_anonymous_functions' => true]
3✔
154
                ),
3✔
155
            ]
3✔
156
        );
3✔
157
    }
158

159
    public function isCandidate(Tokens $tokens): bool
160
    {
161
        return $tokens->isTokenKindFound('{');
54✔
162
    }
163

164
    /**
165
     * {@inheritdoc}
166
     *
167
     * Must run before SingleLineEmptyBodyFixer, StatementIndentationFixer.
168
     * Must run after ControlStructureBracesFixer, MultilinePromotedPropertiesFixer, NoMultipleStatementsPerLineFixer.
169
     */
170
    public function getPriority(): int
171
    {
172
        return -2;
1✔
173
    }
174

175
    /** @protected */
176
    public function createConfigurationDefinition(): FixerConfigurationResolverInterface
177
    {
178
        return new FixerConfigurationResolver([
63✔
179
            (new FixerOptionBuilder('control_structures_opening_brace', 'The position of the opening brace of control structures‘ body.'))
63✔
180
                ->setAllowedValues([self::NEXT_LINE_UNLESS_NEWLINE_AT_SIGNATURE_END, self::SAME_LINE])
63✔
181
                ->setDefault(self::SAME_LINE)
63✔
182
                ->getOption(),
63✔
183
            (new FixerOptionBuilder('functions_opening_brace', 'The position of the opening brace of functions‘ body.'))
63✔
184
                ->setAllowedValues([self::NEXT_LINE_UNLESS_NEWLINE_AT_SIGNATURE_END, self::SAME_LINE])
63✔
185
                ->setDefault(self::NEXT_LINE_UNLESS_NEWLINE_AT_SIGNATURE_END)
63✔
186
                ->getOption(),
63✔
187
            (new FixerOptionBuilder('anonymous_functions_opening_brace', 'The position of the opening brace of anonymous functions‘ body.'))
63✔
188
                ->setAllowedValues([self::NEXT_LINE_UNLESS_NEWLINE_AT_SIGNATURE_END, self::SAME_LINE])
63✔
189
                ->setDefault(self::SAME_LINE)
63✔
190
                ->getOption(),
63✔
191
            (new FixerOptionBuilder('classes_opening_brace', 'The position of the opening brace of classes‘ body.'))
63✔
192
                ->setAllowedValues([self::NEXT_LINE_UNLESS_NEWLINE_AT_SIGNATURE_END, self::SAME_LINE])
63✔
193
                ->setDefault(self::NEXT_LINE_UNLESS_NEWLINE_AT_SIGNATURE_END)
63✔
194
                ->getOption(),
63✔
195
            (new FixerOptionBuilder('anonymous_classes_opening_brace', 'The position of the opening brace of anonymous classes‘ body.'))
63✔
196
                ->setAllowedValues([self::NEXT_LINE_UNLESS_NEWLINE_AT_SIGNATURE_END, self::SAME_LINE])
63✔
197
                ->setDefault(self::SAME_LINE)
63✔
198
                ->getOption(),
63✔
199
            (new FixerOptionBuilder('allow_single_line_empty_anonymous_classes', 'Allow anonymous classes to have opening and closing braces on the same line.'))
63✔
200
                ->setAllowedTypes(['bool'])
63✔
201
                ->setDefault(true)
63✔
202
                ->getOption(),
63✔
203
            (new FixerOptionBuilder('allow_single_line_anonymous_functions', 'Allow anonymous functions to have opening and closing braces on the same line.'))
63✔
204
                ->setAllowedTypes(['bool'])
63✔
205
                ->setDefault(true)
63✔
206
                ->getOption(),
63✔
207
        ]);
63✔
208
    }
209

210
    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
211
    {
212
        $classyTokens = Token::getClassyTokenKinds();
54✔
213
        $tokensAnalyzer = new TokensAnalyzer($tokens);
54✔
214

215
        $allowSingleLineUntil = null;
54✔
216

217
        foreach ($tokens as $index => $token) {
54✔
218
            $allowSingleLine = false;
54✔
219
            $allowSingleLineIfEmpty = false;
54✔
220

221
            if ($token->isGivenKind($classyTokens)) {
54✔
222
                $openBraceIndex = $tokens->getNextTokenOfKind($index, ['{']);
10✔
223

224
                if ($tokensAnalyzer->isAnonymousClass($index)) {
10✔
225
                    $allowSingleLineIfEmpty = true === $this->configuration['allow_single_line_empty_anonymous_classes'];
3✔
226
                    $positionOption = 'anonymous_classes_opening_brace';
3✔
227
                } else {
228
                    $positionOption = 'classes_opening_brace';
8✔
229
                }
230
            } elseif ($token->isGivenKind(T_FUNCTION)) {
54✔
231
                $openBraceIndex = $tokens->getNextTokenOfKind($index, ['{', ';', [CT::T_PROPERTY_HOOK_BRACE_OPEN]]);
23✔
232

233
                if (!$tokens[$openBraceIndex]->equals('{')) {
23✔
234
                    continue;
1✔
235
                }
236

237
                if ($tokensAnalyzer->isLambda($index)) {
22✔
238
                    $allowSingleLine = true === $this->configuration['allow_single_line_anonymous_functions'];
4✔
239
                    $positionOption = 'anonymous_functions_opening_brace';
4✔
240
                } else {
241
                    $positionOption = 'functions_opening_brace';
20✔
242
                }
243
            } elseif ($token->isGivenKind(self::CONTROL_STRUCTURE_TOKENS)) {
54✔
244
                $parenthesisEndIndex = $this->findParenthesisEnd($tokens, $index);
29✔
245
                $openBraceIndex = $tokens->getNextMeaningfulToken($parenthesisEndIndex);
29✔
246

247
                if (!$tokens[$openBraceIndex]->equals('{')) {
29✔
248
                    continue;
4✔
249
                }
250

251
                $positionOption = 'control_structures_opening_brace';
29✔
252
            } else {
253
                continue;
54✔
254
            }
255

256
            $closeBraceIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $openBraceIndex);
54✔
257

258
            $addNewlinesInsideBraces = true;
54✔
259
            if ($allowSingleLine || $allowSingleLineIfEmpty || $index < $allowSingleLineUntil) {
54✔
260
                $addNewlinesInsideBraces = false;
6✔
261

262
                for ($indexInsideBraces = $openBraceIndex + 1; $indexInsideBraces < $closeBraceIndex; ++$indexInsideBraces) {
6✔
263
                    $tokenInsideBraces = $tokens[$indexInsideBraces];
5✔
264

265
                    if (
266
                        ($allowSingleLineIfEmpty && !$tokenInsideBraces->isWhitespace() && !$tokenInsideBraces->isComment())
5✔
267
                        || ($tokenInsideBraces->isWhitespace() && Preg::match('/\R/', $tokenInsideBraces->getContent()))
5✔
268
                    ) {
269
                        $addNewlinesInsideBraces = true;
5✔
270

271
                        break;
5✔
272
                    }
273
                }
274

275
                if (!$addNewlinesInsideBraces && null === $allowSingleLineUntil) {
6✔
276
                    $allowSingleLineUntil = $closeBraceIndex;
2✔
277
                }
278
            }
279

280
            if (
281
                $addNewlinesInsideBraces
54✔
282
                && !$this->isFollowedByNewLine($tokens, $openBraceIndex)
54✔
283
                && !$this->hasCommentOnSameLine($tokens, $openBraceIndex)
54✔
284
                && !$tokens[$tokens->getNextMeaningfulToken($openBraceIndex)]->isGivenKind(T_CLOSE_TAG)
54✔
285
            ) {
286
                $whitespace = $this->whitespacesConfig->getLineEnding().$this->getLineIndentation($tokens, $openBraceIndex);
1✔
287
                if ($tokens->ensureWhitespaceAtIndex($openBraceIndex + 1, 0, $whitespace)) {
1✔
288
                    ++$closeBraceIndex;
×
289
                }
290
            }
291

292
            $whitespace = ' ';
54✔
293
            if (self::NEXT_LINE_UNLESS_NEWLINE_AT_SIGNATURE_END === $this->configuration[$positionOption]) {
54✔
294
                $whitespace = $this->whitespacesConfig->getLineEnding().$this->getLineIndentation($tokens, $index);
35✔
295

296
                $previousTokenIndex = $openBraceIndex;
35✔
297
                do {
298
                    $previousTokenIndex = $tokens->getPrevMeaningfulToken($previousTokenIndex);
35✔
299
                } while ($tokens[$previousTokenIndex]->isGivenKind([CT::T_TYPE_COLON, CT::T_NULLABLE_TYPE, T_STRING, T_NS_SEPARATOR, CT::T_ARRAY_TYPEHINT, T_STATIC, CT::T_TYPE_ALTERNATION, CT::T_TYPE_INTERSECTION, T_CALLABLE, CT::T_DISJUNCTIVE_NORMAL_FORM_TYPE_PARENTHESIS_OPEN, CT::T_DISJUNCTIVE_NORMAL_FORM_TYPE_PARENTHESIS_CLOSE]));
35✔
300

301
                if ($tokens[$previousTokenIndex]->equals(')')) {
35✔
302
                    if ($tokens[--$previousTokenIndex]->isComment()) {
32✔
303
                        --$previousTokenIndex;
×
304
                    }
305
                    if (
306
                        $tokens[$previousTokenIndex]->isWhitespace()
32✔
307
                        && Preg::match('/\R/', $tokens[$previousTokenIndex]->getContent())
32✔
308
                    ) {
309
                        $whitespace = ' ';
13✔
310
                    }
311
                }
312
            }
313

314
            $moveBraceToIndex = null;
54✔
315

316
            if (' ' === $whitespace) {
54✔
317
                $previousMeaningfulIndex = $tokens->getPrevMeaningfulToken($openBraceIndex);
34✔
318
                for ($indexBeforeOpenBrace = $openBraceIndex - 1; $indexBeforeOpenBrace > $previousMeaningfulIndex; --$indexBeforeOpenBrace) {
34✔
319
                    if (!$tokens[$indexBeforeOpenBrace]->isComment()) {
34✔
320
                        continue;
34✔
321
                    }
322

323
                    $tokenBeforeOpenBrace = $tokens[--$indexBeforeOpenBrace];
4✔
324
                    if ($tokenBeforeOpenBrace->isWhitespace()) {
4✔
325
                        $moveBraceToIndex = $indexBeforeOpenBrace;
3✔
326
                    } elseif ($indexBeforeOpenBrace === $previousMeaningfulIndex) {
1✔
327
                        $moveBraceToIndex = $previousMeaningfulIndex + 1;
1✔
328
                    }
329
                }
330
            } elseif (!$tokens[$openBraceIndex - 1]->isWhitespace() || !Preg::match('/\R/', $tokens[$openBraceIndex - 1]->getContent())) {
23✔
331
                for ($indexAfterOpenBrace = $openBraceIndex + 1; $indexAfterOpenBrace < $closeBraceIndex; ++$indexAfterOpenBrace) {
21✔
332
                    if ($tokens[$indexAfterOpenBrace]->isWhitespace() && Preg::match('/\R/', $tokens[$indexAfterOpenBrace]->getContent())) {
21✔
333
                        break;
21✔
334
                    }
335

336
                    if ($tokens[$indexAfterOpenBrace]->isComment() && !str_starts_with($tokens[$indexAfterOpenBrace]->getContent(), '/*')) {
3✔
337
                        $moveBraceToIndex = $indexAfterOpenBrace + 1;
3✔
338
                    }
339
                }
340
            }
341

342
            if (null !== $moveBraceToIndex) {
54✔
343
                /** @var Token $movedToken */
344
                $movedToken = clone $tokens[$openBraceIndex];
7✔
345

346
                $delta = $openBraceIndex < $moveBraceToIndex ? 1 : -1;
7✔
347

348
                if ($tokens[$openBraceIndex + $delta]->isWhitespace()) {
7✔
349
                    if (-1 === $delta && Preg::match('/\R/', $tokens[$openBraceIndex - 1]->getContent())) {
6✔
350
                        $content = Preg::replace('/^(\h*?\R)?\h*/', '', $tokens[$openBraceIndex + 1]->getContent());
2✔
351
                        if ('' !== $content) {
2✔
352
                            $tokens[$openBraceIndex + 1] = new Token([T_WHITESPACE, $content]);
1✔
353
                        } else {
354
                            $tokens->clearAt($openBraceIndex + 1);
1✔
355
                        }
356
                    } elseif ($tokens[$openBraceIndex - 1]->isWhitespace()) {
4✔
357
                        $tokens->clearAt($openBraceIndex - 1);
3✔
358
                    }
359
                }
360

361
                for ($i = $openBraceIndex; $i !== $moveBraceToIndex; $i += $delta) {
7✔
362
                    /** @var Token $siblingToken */
363
                    $siblingToken = $tokens[$i + $delta];
7✔
364
                    $tokens[$i] = $siblingToken;
7✔
365
                }
366

367
                $tokens[$i] = $movedToken;
7✔
368

369
                if ($tokens[$openBraceIndex]->isWhitespace() && $tokens[$openBraceIndex + 1]->isWhitespace()) {
7✔
370
                    $tokens[$openBraceIndex] = new Token([
4✔
371
                        T_WHITESPACE,
4✔
372
                        $tokens[$openBraceIndex]->getContent().$tokens[$openBraceIndex + 1]->getContent(),
4✔
373
                    ]);
4✔
374
                    $tokens->clearAt($openBraceIndex + 1);
4✔
375
                }
376

377
                $openBraceIndex = $moveBraceToIndex;
7✔
378
            }
379

380
            if ($tokens->ensureWhitespaceAtIndex($openBraceIndex - 1, 1, $whitespace)) {
54✔
381
                ++$closeBraceIndex;
5✔
382
                if (null !== $allowSingleLineUntil) {
5✔
383
                    ++$allowSingleLineUntil;
×
384
                }
385
            }
386

387
            if (
388
                !$addNewlinesInsideBraces
54✔
389
                || $tokens[$tokens->getPrevMeaningfulToken($closeBraceIndex)]->isGivenKind(T_OPEN_TAG)
54✔
390
            ) {
391
                continue;
2✔
392
            }
393

394
            $prevIndex = $closeBraceIndex - 1;
54✔
395
            while ($tokens->isEmptyAt($prevIndex)) {
54✔
396
                --$prevIndex;
×
397
            }
398

399
            $prevToken = $tokens[$prevIndex];
54✔
400

401
            if ($prevToken->isWhitespace() && Preg::match('/\R/', $prevToken->getContent())) {
54✔
402
                continue;
54✔
403
            }
404

405
            $whitespace = $this->whitespacesConfig->getLineEnding().$this->getLineIndentation($tokens, $openBraceIndex);
4✔
406
            $tokens->ensureWhitespaceAtIndex($prevIndex, 1, $whitespace);
4✔
407
        }
408
    }
409

410
    private function findParenthesisEnd(Tokens $tokens, int $structureTokenIndex): int
411
    {
412
        $nextIndex = $tokens->getNextMeaningfulToken($structureTokenIndex);
29✔
413
        $nextToken = $tokens[$nextIndex];
29✔
414

415
        // return if next token is not opening parenthesis
416
        if (!$nextToken->equals('(')) {
29✔
417
            return $structureTokenIndex;
8✔
418
        }
419

420
        return $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $nextIndex);
29✔
421
    }
422

423
    private function isFollowedByNewLine(Tokens $tokens, int $index): bool
424
    {
425
        for (++$index, $max = \count($tokens) - 1; $index < $max; ++$index) {
54✔
426
            $token = $tokens[$index];
54✔
427
            if (!$token->isComment()) {
54✔
428
                return $token->isWhitespace() && Preg::match('/\R/', $token->getContent());
54✔
429
            }
430
        }
431

432
        return false;
×
433
    }
434

435
    private function hasCommentOnSameLine(Tokens $tokens, int $index): bool
436
    {
437
        $token = $tokens[$index + 1];
6✔
438

439
        if ($token->isWhitespace() && !Preg::match('/\R/', $token->getContent())) {
6✔
440
            $token = $tokens[$index + 2];
6✔
441
        }
442

443
        return $token->isComment();
6✔
444
    }
445
}
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