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

keradus / PHP-CS-Fixer / 17377459942

01 Sep 2025 12:19PM UTC coverage: 94.684% (-0.009%) from 94.693%
17377459942

push

github

web-flow
chore: `Tokens::offsetSet` - explicit validation of input (#9004)

1 of 5 new or added lines in 1 file covered. (20.0%)

306 existing lines in 60 files now uncovered.

28390 of 29984 relevant lines covered (94.68%)

45.5 hits per line

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

94.37
/src/Fixer/ClassNotation/ClassAttributesSeparationFixer.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\ClassNotation;
16

17
use PhpCsFixer\AbstractFixer;
18
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
19
use PhpCsFixer\Fixer\ConfigurableFixerTrait;
20
use PhpCsFixer\Fixer\WhitespacesAwareFixerInterface;
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\Preg;
30
use PhpCsFixer\Tokenizer\CT;
31
use PhpCsFixer\Tokenizer\FCT;
32
use PhpCsFixer\Tokenizer\Token;
33
use PhpCsFixer\Tokenizer\Tokens;
34
use PhpCsFixer\Tokenizer\TokensAnalyzer;
35
use PhpCsFixer\Utils;
36
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
37

38
/**
39
 * Make sure there is one blank line above and below class elements.
40
 *
41
 * The exception is when an element is the first or last item in a 'classy'.
42
 *
43
 * @phpstan-type _Class array{
44
 *      index: int,
45
 *      open: int,
46
 *      close: int,
47
 *      elements: non-empty-list<_Element>
48
 *  }
49
 * @phpstan-type _Element array{token: Token, type: string, index: int, start: int, end: int}
50
 * @phpstan-type _AutogeneratedInputConfiguration array{
51
 *  elements?: array<string, string>,
52
 * }
53
 * @phpstan-type _AutogeneratedComputedConfiguration array{
54
 *  elements: array<string, string>,
55
 * }
56
 *
57
 * @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
58
 *
59
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
60
 */
61
final class ClassAttributesSeparationFixer extends AbstractFixer implements ConfigurableFixerInterface, WhitespacesAwareFixerInterface
62
{
63
    /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
64
    use ConfigurableFixerTrait;
65

66
    /**
67
     * @internal
68
     */
69
    public const SPACING_NONE = 'none';
70

71
    /**
72
     * @internal
73
     */
74
    public const SPACING_ONE = 'one';
75

76
    private const SPACING_ONLY_IF_META = 'only_if_meta';
77
    private const MODIFIER_TYPES = [\T_PRIVATE, \T_PROTECTED, \T_PUBLIC, \T_ABSTRACT, \T_FINAL, \T_STATIC, \T_STRING, \T_NS_SEPARATOR, \T_VAR, CT::T_NULLABLE_TYPE, CT::T_ARRAY_TYPEHINT, CT::T_TYPE_ALTERNATION, CT::T_TYPE_INTERSECTION, CT::T_DISJUNCTIVE_NORMAL_FORM_TYPE_PARENTHESIS_OPEN, CT::T_DISJUNCTIVE_NORMAL_FORM_TYPE_PARENTHESIS_CLOSE, FCT::T_READONLY, FCT::T_PRIVATE_SET, FCT::T_PROTECTED_SET, FCT::T_PUBLIC_SET];
78

79
    /**
80
     * @var array<string, string>
81
     */
82
    private array $classElementTypes = [];
83

84
    public function getDefinition(): FixerDefinitionInterface
85
    {
86
        return new FixerDefinition(
3✔
87
            'Class, trait and interface elements must be separated with one or none blank line.',
3✔
88
            [
3✔
89
                new CodeSample(
3✔
90
                    <<<'PHP'
3✔
91
                        <?php
92
                        final class Sample
93
                        {
94
                            protected function foo()
95
                            {
96
                            }
97
                            protected function bar()
98
                            {
99
                            }
100

101

102
                        }
103

104
                        PHP
3✔
105
                ),
3✔
106
                new CodeSample(
3✔
107
                    <<<'PHP'
3✔
108
                        <?php
109
                        class Sample
110
                        {private $a; // foo
111
                            /** second in a hour */
112
                            private $b;
113
                        }
114

115
                        PHP,
3✔
116
                    ['elements' => ['property' => self::SPACING_ONE]]
3✔
117
                ),
3✔
118
                new CodeSample(
3✔
119
                    <<<'PHP'
3✔
120
                        <?php
121
                        class Sample
122
                        {
123
                            const A = 1;
124
                            /** seconds in some hours */
125
                            const B = 3600;
126
                        }
127

128
                        PHP,
3✔
129
                    ['elements' => ['const' => self::SPACING_ONE]]
3✔
130
                ),
3✔
131
                new CodeSample(
3✔
132
                    <<<'PHP'
3✔
133
                        <?php
134
                        class Sample
135
                        {
136
                            /** @var int */
137
                            const SECOND = 1;
138
                            /** @var int */
139
                            const MINUTE = 60;
140

141
                            const HOUR = 3600;
142

143
                            const DAY = 86400;
144
                        }
145

146
                        PHP,
3✔
147
                    ['elements' => ['const' => self::SPACING_ONLY_IF_META]]
3✔
148
                ),
3✔
149
                new VersionSpecificCodeSample(
3✔
150
                    <<<'PHP'
3✔
151
                        <?php
152
                        class Sample
153
                        {
154
                            public $a;
155
                            #[SetUp]
156
                            public $b;
157
                            /** @var string */
158
                            public $c;
159
                            /** @internal */
160
                            #[Assert\String()]
161
                            public $d;
162

163
                            public $e;
164
                        }
165

166
                        PHP,
3✔
167
                    new VersionSpecification(8_00_00),
3✔
168
                    ['elements' => ['property' => self::SPACING_ONLY_IF_META]]
3✔
169
                ),
3✔
170
            ]
3✔
171
        );
3✔
172
    }
173

174
    /**
175
     * {@inheritdoc}
176
     *
177
     * Must run before BracesFixer, IndentationTypeFixer, NoExtraBlankLinesFixer, StatementIndentationFixer.
178
     * Must run after OrderedClassElementsFixer, PhpUnitDataProviderMethodOrderFixer, SingleClassElementPerStatementFixer, VisibilityRequiredFixer.
179
     */
180
    public function getPriority(): int
181
    {
182
        return 55;
1✔
183
    }
184

185
    public function isCandidate(Tokens $tokens): bool
186
    {
187
        return $tokens->isAnyTokenKindsFound(Token::getClassyTokenKinds());
95✔
188
    }
189

190
    protected function configurePostNormalisation(): void
191
    {
192
        $this->classElementTypes = []; // reset previous configuration
114✔
193

194
        foreach ($this->configuration['elements'] as $elementType => $spacing) {
114✔
195
            $this->classElementTypes[$elementType] = $spacing;
114✔
196
        }
197
    }
198

199
    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
200
    {
201
        foreach ($this->getElementsByClass($tokens) as $class) {
95✔
202
            $elements = $class['elements'];
93✔
203

204
            if (0 === \count($elements)) {
93✔
UNCOV
205
                continue;
×
206
            }
207

208
            if (isset($this->classElementTypes[$elements[0]['type']])) {
93✔
209
                $this->fixSpaceBelowClassElement($tokens, $class);
89✔
210
            }
211

212
            foreach ($elements as $index => $element) {
93✔
213
                if (isset($this->classElementTypes[$element['type']])) {
93✔
214
                    $this->fixSpaceAboveClassElement($tokens, $class, $index);
92✔
215
                }
216
            }
217
        }
218
    }
219

220
    protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
221
    {
222
        return new FixerConfigurationResolver([
114✔
223
            (new FixerOptionBuilder('elements', 'Dictionary of `const|method|property|trait_import|case` => `none|one|only_if_meta` values.'))
114✔
224
                ->setAllowedTypes(['array<string, string>'])
114✔
225
                ->setAllowedValues([static function (array $option): bool {
114✔
226
                    foreach ($option as $type => $spacing) {
114✔
227
                        $supportedTypes = ['const', 'method', 'property', 'trait_import', 'case'];
114✔
228

229
                        if (!\in_array($type, $supportedTypes, true)) {
114✔
230
                            throw new InvalidOptionsException(
2✔
231
                                \sprintf(
2✔
232
                                    'Unexpected element type, expected any of %s, got "%s".',
2✔
233
                                    Utils::naturalLanguageJoin($supportedTypes),
2✔
234
                                    \gettype($type).'#'.$type
2✔
235
                                )
2✔
236
                            );
2✔
237
                        }
238

239
                        $supportedSpacings = [self::SPACING_NONE, self::SPACING_ONE, self::SPACING_ONLY_IF_META];
114✔
240

241
                        if (!\in_array($spacing, $supportedSpacings, true)) {
114✔
242
                            throw new InvalidOptionsException(
1✔
243
                                \sprintf(
1✔
244
                                    'Unexpected spacing for element type "%s", expected any of %s, got "%s".',
1✔
245
                                    $spacing,
1✔
246
                                    Utils::naturalLanguageJoin($supportedSpacings),
1✔
247
                                    \is_object($spacing) ? \get_class($spacing) : (null === $spacing ? 'null' : \gettype($spacing).'#'.$spacing)
1✔
248
                                )
1✔
249
                            );
1✔
250
                        }
251
                    }
252

253
                    return true;
114✔
254
                }])
114✔
255
                ->setDefault([
114✔
256
                    'const' => self::SPACING_ONE,
114✔
257
                    'method' => self::SPACING_ONE,
114✔
258
                    'property' => self::SPACING_ONE,
114✔
259
                    'trait_import' => self::SPACING_NONE,
114✔
260
                    'case' => self::SPACING_NONE,
114✔
261
                ])
114✔
262
                ->getOption(),
114✔
263
        ]);
114✔
264
    }
265

266
    /**
267
     * Fix spacing above an element of a class, interface or trait.
268
     *
269
     * Deals with comments, PHPDocs and spaces above the element with respect to the position of the
270
     * element within the class, interface or trait.
271
     *
272
     * @param _Class $class
273
     */
274
    private function fixSpaceAboveClassElement(Tokens $tokens, array $class, int $elementIndex): void
275
    {
276
        \assert(isset($class['elements'][$elementIndex]));
92✔
277
        $element = $class['elements'][$elementIndex];
92✔
278
        $elementAboveEnd = isset($class['elements'][$elementIndex + 1]) ? $class['elements'][$elementIndex + 1]['end'] : 0;
92✔
279
        $nonWhiteAbove = $tokens->getPrevNonWhitespace($element['start']);
92✔
280

281
        // element is directly after class open brace
282
        if ($nonWhiteAbove === $class['open']) {
92✔
283
            $this->correctLineBreaks($tokens, $nonWhiteAbove, $element['start'], 1);
65✔
284

285
            return;
65✔
286
        }
287

288
        // deal with comments above an element
289
        if ($tokens[$nonWhiteAbove]->isGivenKind(\T_COMMENT)) {
81✔
290
            // check if the comment belongs to the previous element
291
            if ($elementAboveEnd === $nonWhiteAbove) {
25✔
292
                $this->correctLineBreaks($tokens, $nonWhiteAbove, $element['start'], $this->determineRequiredLineCount($tokens, $class, $elementIndex));
8✔
293

294
                return;
8✔
295
            }
296

297
            // more than one line break, always bring it back to 2 line breaks between the element start and what is above it
298
            if ($tokens[$nonWhiteAbove + 1]->isWhitespace() && substr_count($tokens[$nonWhiteAbove + 1]->getContent(), "\n") > 1) {
19✔
299
                $this->correctLineBreaks($tokens, $nonWhiteAbove, $element['start'], 2);
8✔
300

301
                return;
8✔
302
            }
303

304
            // there are 2 cases:
305
            if (
306
                1 === $element['start'] - $nonWhiteAbove
13✔
307
                || $tokens[$nonWhiteAbove - 1]->isWhitespace() && substr_count($tokens[$nonWhiteAbove - 1]->getContent(), "\n") > 0
13✔
308
                || $tokens[$nonWhiteAbove + 1]->isWhitespace() && substr_count($tokens[$nonWhiteAbove + 1]->getContent(), "\n") > 0
13✔
309
            ) {
310
                // 1. The comment is meant for the element (although not a PHPDoc),
311
                //    make sure there is one line break between the element and the comment...
312
                $this->correctLineBreaks($tokens, $nonWhiteAbove, $element['start'], 1);
13✔
313
                //    ... and make sure there is blank line above the comment (with the exception when it is directly after a class opening)
314
                $nonWhiteAbove = $this->findCommentBlockStart($tokens, $nonWhiteAbove, $elementAboveEnd);
13✔
315
                $nonWhiteAboveComment = $tokens->getPrevNonWhitespace($nonWhiteAbove);
13✔
316

317
                if ($nonWhiteAboveComment === $class['open']) {
13✔
318
                    if ($tokens[$nonWhiteAboveComment - 1]->isWhitespace() && substr_count($tokens[$nonWhiteAboveComment - 1]->getContent(), "\n") > 0) {
10✔
319
                        $this->correctLineBreaks($tokens, $nonWhiteAboveComment, $nonWhiteAbove, 1);
7✔
320
                    }
321
                } else {
322
                    $this->correctLineBreaks($tokens, $nonWhiteAboveComment, $nonWhiteAbove, 2);
3✔
323
                }
324
            } else {
325
                // 2. The comment belongs to the code above the element,
326
                //    make sure there is a blank line above the element (i.e. 2 line breaks)
UNCOV
327
                $this->correctLineBreaks($tokens, $nonWhiteAbove, $element['start'], 2);
×
328
            }
329

330
            return;
13✔
331
        }
332

333
        // deal with element with a PHPDoc/attribute above it
334
        if ($tokens[$nonWhiteAbove]->isGivenKind([\T_DOC_COMMENT, CT::T_ATTRIBUTE_CLOSE])) {
64✔
335
            // there should be one linebreak between the element and the attribute above it
336
            $this->correctLineBreaks($tokens, $nonWhiteAbove, $element['start'], 1);
24✔
337

338
            // make sure there is blank line above the comment (with the exception when it is directly after a class opening)
339
            $nonWhiteAbove = $this->findCommentBlockStart($tokens, $nonWhiteAbove, $elementAboveEnd);
24✔
340
            $nonWhiteAboveComment = $tokens->getPrevNonWhitespace($nonWhiteAbove);
24✔
341

342
            $this->correctLineBreaks($tokens, $nonWhiteAboveComment, $nonWhiteAbove, $nonWhiteAboveComment === $class['open'] ? 1 : 2);
24✔
343

344
            return;
24✔
345
        }
346

347
        $this->correctLineBreaks($tokens, $nonWhiteAbove, $element['start'], $this->determineRequiredLineCount($tokens, $class, $elementIndex));
54✔
348
    }
349

350
    /**
351
     * @param _Class $class
352
     */
353
    private function determineRequiredLineCount(Tokens $tokens, array $class, int $elementIndex): int
354
    {
355
        \assert(isset($class['elements'][$elementIndex]));
55✔
356
        $type = $class['elements'][$elementIndex]['type'];
55✔
357
        $spacing = $this->classElementTypes[$type];
55✔
358

359
        if (self::SPACING_ONE === $spacing) {
55✔
360
            return 2;
40✔
361
        }
362

363
        if (self::SPACING_NONE === $spacing) {
21✔
364
            if (!isset($class['elements'][$elementIndex + 1])) {
14✔
UNCOV
365
                return 1;
×
366
            }
367

368
            $aboveElement = $class['elements'][$elementIndex + 1];
14✔
369

370
            if ($aboveElement['type'] !== $type) {
14✔
371
                return 2;
2✔
372
            }
373

374
            $aboveElementDocCandidateIndex = $tokens->getPrevNonWhitespace($aboveElement['start']);
14✔
375

376
            return $tokens[$aboveElementDocCandidateIndex]->isGivenKind([\T_DOC_COMMENT, CT::T_ATTRIBUTE_CLOSE]) ? 2 : 1;
14✔
377
        }
378

379
        if (self::SPACING_ONLY_IF_META === $spacing) {
7✔
380
            $aboveElementDocCandidateIndex = $tokens->getPrevNonWhitespace($class['elements'][$elementIndex]['start']);
7✔
381

382
            return $tokens[$aboveElementDocCandidateIndex]->isGivenKind([\T_DOC_COMMENT, CT::T_ATTRIBUTE_CLOSE]) ? 2 : 1;
7✔
383
        }
384

UNCOV
385
        throw new \RuntimeException(\sprintf('Unknown spacing "%s".', $spacing));
×
386
    }
387

388
    /**
389
     * @param _Class $class
390
     */
391
    private function fixSpaceBelowClassElement(Tokens $tokens, array $class): void
392
    {
393
        $element = $class['elements'][0];
89✔
394

395
        // if this is last element fix; fix to the class end `}` here if appropriate
396
        if ($class['close'] === $tokens->getNextNonWhitespace($element['end'])) {
89✔
397
            $this->correctLineBreaks($tokens, $element['end'], $class['close'], 1);
88✔
398
        }
399
    }
400

401
    private function correctLineBreaks(Tokens $tokens, int $startIndex, int $endIndex, int $reqLineCount): void
402
    {
403
        $lineEnding = $this->whitespacesConfig->getLineEnding();
92✔
404

405
        ++$startIndex;
92✔
406
        $numbOfWhiteTokens = $endIndex - $startIndex;
92✔
407

408
        if (0 === $numbOfWhiteTokens) {
92✔
409
            $tokens->insertAt($startIndex, new Token([\T_WHITESPACE, str_repeat($lineEnding, $reqLineCount)]));
17✔
410

411
            return;
17✔
412
        }
413

414
        $lineBreakCount = $this->getLineBreakCount($tokens, $startIndex, $endIndex);
92✔
415

416
        if ($reqLineCount === $lineBreakCount) {
92✔
417
            return;
92✔
418
        }
419

420
        if ($lineBreakCount < $reqLineCount) {
69✔
421
            $tokens[$startIndex] = new Token([
50✔
422
                \T_WHITESPACE,
50✔
423
                str_repeat($lineEnding, $reqLineCount - $lineBreakCount).$tokens[$startIndex]->getContent(),
50✔
424
            ]);
50✔
425

426
            return;
50✔
427
        }
428

429
        // $lineCount = > $reqLineCount : check the one Token case first since this one will be true most of the time
430
        if (1 === $numbOfWhiteTokens) {
39✔
431
            $tokens[$startIndex] = new Token([
39✔
432
                \T_WHITESPACE,
39✔
433
                Preg::replace('/\r\n|\n/', '', $tokens[$startIndex]->getContent(), $lineBreakCount - $reqLineCount),
39✔
434
            ]);
39✔
435

436
            return;
39✔
437
        }
438

439
        // $numbOfWhiteTokens = > 1
440
        $toReplaceCount = $lineBreakCount - $reqLineCount;
×
441

UNCOV
442
        for ($i = $startIndex; $i < $endIndex && $toReplaceCount > 0; ++$i) {
×
UNCOV
443
            $tokenLineCount = substr_count($tokens[$i]->getContent(), "\n");
×
444

UNCOV
445
            if ($tokenLineCount > 0) {
×
UNCOV
446
                $tokens[$i] = new Token([
×
UNCOV
447
                    \T_WHITESPACE,
×
UNCOV
448
                    Preg::replace('/\r\n|\n/', '', $tokens[$i]->getContent(), min($toReplaceCount, $tokenLineCount)),
×
UNCOV
449
                ]);
×
UNCOV
450
                $toReplaceCount -= $tokenLineCount;
×
451
            }
452
        }
453
    }
454

455
    private function getLineBreakCount(Tokens $tokens, int $startIndex, int $endIndex): int
456
    {
457
        $lineCount = 0;
100✔
458

459
        for ($i = $startIndex; $i < $endIndex; ++$i) {
100✔
460
            $lineCount += substr_count($tokens[$i]->getContent(), "\n");
100✔
461
        }
462

463
        return $lineCount;
100✔
464
    }
465

466
    private function findCommentBlockStart(Tokens $tokens, int $start, int $elementAboveEnd): int
467
    {
468
        for ($i = $start; $i > $elementAboveEnd; --$i) {
44✔
469
            if ($tokens[$i]->isGivenKind(CT::T_ATTRIBUTE_CLOSE)) {
44✔
470
                $start = $i = $tokens->findBlockStart(Tokens::BLOCK_TYPE_ATTRIBUTE, $i);
7✔
471

472
                continue;
7✔
473
            }
474

475
            if ($tokens[$i]->isComment()) {
44✔
476
                $start = $i;
39✔
477

478
                continue;
39✔
479
            }
480

481
            if (!$tokens[$i]->isWhitespace() || $this->getLineBreakCount($tokens, $i, $i + 1) > 1) {
44✔
482
                break;
40✔
483
            }
484
        }
485

486
        return $start;
44✔
487
    }
488

489
    /**
490
     * @TODO Introduce proper DTO instead of an array
491
     *
492
     * @return \Generator<_Class>
493
     */
494
    private function getElementsByClass(Tokens $tokens): \Generator
495
    {
496
        $tokensAnalyzer = new TokensAnalyzer($tokens);
95✔
497
        $class = null;
95✔
498

499
        foreach (array_reverse($tokensAnalyzer->getClassyElements(), true) as $index => $element) {
95✔
500
            $element['index'] = $index;
93✔
501

502
            if (null === $class || $element['classIndex'] !== $class['index']) {
93✔
503
                if (null !== $class) {
93✔
504
                    yield $class;
4✔
505
                }
506

507
                $classIndex = $element['classIndex'];
93✔
508
                $classOpen = $tokens->getNextTokenOfKind($classIndex, ['{']);
93✔
509
                $classEnd = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $classOpen);
93✔
510
                $class = [
93✔
511
                    'index' => $element['classIndex'],
93✔
512
                    'open' => $classOpen,
93✔
513
                    'close' => $classEnd,
93✔
514
                    'elements' => [],
93✔
515
                ];
93✔
516
            }
517

518
            unset($element['classIndex']);
93✔
519
            $element['start'] = $this->getFirstTokenIndexOfClassElement($tokens, $class['open'], $index);
93✔
520
            $element['end'] = $this->getLastTokenIndexOfClassElement($tokens, $class['index'], $index, $element['type'], $tokensAnalyzer);
93✔
521

522
            $class['elements'][] = $element; // reset the key by design
93✔
523
        }
524

525
        if (null !== $class) {
95✔
526
            yield $class;
93✔
527
        }
528
    }
529

530
    /**
531
     * including trailing single line comments if belonging to the class element.
532
     */
533
    private function getFirstTokenIndexOfClassElement(Tokens $tokens, int $classOpen, int $elementIndex): int
534
    {
535
        $firstElementAttributeIndex = $elementIndex;
93✔
536

537
        do {
538
            $nonWhiteAbove = $tokens->getPrevMeaningfulToken($firstElementAttributeIndex);
93✔
539

540
            if (null !== $nonWhiteAbove && $tokens[$nonWhiteAbove]->isGivenKind(self::MODIFIER_TYPES)) {
93✔
541
                $firstElementAttributeIndex = $nonWhiteAbove;
77✔
542
            } else {
543
                break;
93✔
544
            }
545
        } while ($firstElementAttributeIndex > $classOpen);
77✔
546

547
        return $firstElementAttributeIndex;
93✔
548
    }
549

550
    /**
551
     * including trailing single line comments if belonging to the class element.
552
     */
553
    private function getLastTokenIndexOfClassElement(Tokens $tokens, int $classIndex, int $elementIndex, string $elementType, TokensAnalyzer $tokensAnalyzer): int
554
    {
555
        // find last token of the element
556
        if ('method' === $elementType && !$tokens[$classIndex]->isGivenKind(\T_INTERFACE)) {
93✔
557
            $attributes = $tokensAnalyzer->getMethodAttributes($elementIndex);
53✔
558

559
            if (true === $attributes['abstract']) {
53✔
560
                $elementEndIndex = $tokens->getNextTokenOfKind($elementIndex, [';']);
5✔
561
            } else {
562
                $elementEndIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $tokens->getNextTokenOfKind($elementIndex, ['{']));
51✔
563
            }
564
        } elseif ('trait_import' === $elementType) {
63✔
565
            $elementEndIndex = $elementIndex;
13✔
566

567
            do {
568
                $elementEndIndex = $tokens->getNextMeaningfulToken($elementEndIndex);
13✔
569
            } while ($tokens[$elementEndIndex]->isGivenKind([\T_STRING, \T_NS_SEPARATOR]) || $tokens[$elementEndIndex]->equals(','));
13✔
570

571
            if (!$tokens[$elementEndIndex]->equals(';')) {
13✔
572
                $elementEndIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $tokens->getNextTokenOfKind($elementIndex, ['{']));
4✔
573
            }
574
        } else { // 'const', 'property', enum-'case', or 'method' of an interface
575
            $elementEndIndex = $tokens->getNextTokenOfKind($elementIndex, [';', '{']);
52✔
576
        }
577

578
        $singleLineElement = true;
93✔
579

580
        for ($i = $elementIndex + 1; $i < $elementEndIndex; ++$i) {
93✔
581
            if (str_contains($tokens[$i]->getContent(), "\n")) {
73✔
582
                $singleLineElement = false;
31✔
583

584
                break;
31✔
585
            }
586
        }
587

588
        if ($singleLineElement) {
93✔
589
            while (true) {
77✔
590
                $nextToken = $tokens[$elementEndIndex + 1];
77✔
591

592
                if (($nextToken->isComment() || $nextToken->isWhitespace()) && !str_contains($nextToken->getContent(), "\n")) {
77✔
593
                    ++$elementEndIndex;
14✔
594
                } else {
595
                    break;
77✔
596
                }
597
            }
598

599
            if ($tokens[$elementEndIndex]->isWhitespace()) {
77✔
600
                $elementEndIndex = $tokens->getPrevNonWhitespace($elementEndIndex);
2✔
601
            }
602
        }
603

604
        return $elementEndIndex;
93✔
605
    }
606
}
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