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

keradus / PHP-CS-Fixer / 25961188992

15 May 2026 03:27PM UTC coverage: 93.797% (+0.7%) from 93.053%
25961188992

push

github

web-flow
fix: `MultilinePromotedPropertiesFixer` - fix for `new` in initializers (#9619)

2 of 2 new or added lines in 1 file covered. (100.0%)

49 existing lines in 7 files now uncovered.

29908 of 31886 relevant lines covered (93.8%)

51.7 hits per line

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

95.24
/src/DocBlock/Annotation.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\DocBlock;
16

17
use PhpCsFixer\Preg;
18
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceAnalysis;
19
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis;
20

21
/**
22
 * This represents an entire annotation from a docblock.
23
 *
24
 * @author Graham Campbell <hello@gjcampbell.co.uk>
25
 * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
26
 *
27
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
28
 */
29
final class Annotation implements \Stringable
30
{
31
    /**
32
     * All the annotation tag names with types.
33
     *
34
     * @var non-empty-list<string>
35
     */
36
    public const TAGS_WITH_TYPES = [
37
        'extends',
38
        'implements',
39
        'method',
40
        'param',
41
        'param-out',
42
        'phpstan-import-type',
43
        'phpstan-type',
44
        'phpstan-var',
45
        'property',
46
        'property-read',
47
        'property-write',
48
        'psalm-import-type',
49
        'psalm-type',
50
        'psalm-var',
51
        'return',
52
        'throws',
53
        'type',
54
        'var',
55
    ];
56

57
    /**
58
     * The lines that make up the annotation.
59
     *
60
     * @var non-empty-list<Line>
61
     */
62
    private array $lines;
63

64
    /**
65
     * The position of the first line of the annotation in the docblock.
66
     */
67
    private int $start;
68

69
    /**
70
     * The position of the last line of the annotation in the docblock.
71
     */
72
    private int $end;
73

74
    /**
75
     * The associated tag.
76
     */
77
    private ?Tag $tag = null;
78

79
    /**
80
     * Lazy loaded, cached types content.
81
     */
82
    private ?string $typesContent = null;
83

84
    /**
85
     * The cached types.
86
     *
87
     * @var null|list<string>
88
     */
89
    private ?array $types = null;
90

91
    private ?NamespaceAnalysis $namespace = null;
92

93
    /**
94
     * @var list<NamespaceUseAnalysis>
95
     */
96
    private array $namespaceUses;
97

98
    /**
99
     * Create a new line instance.
100
     *
101
     * @param non-empty-array<int, Line> $lines
102
     * @param null|NamespaceAnalysis     $namespace
103
     * @param list<NamespaceUseAnalysis> $namespaceUses
104
     */
105
    public function __construct(array $lines, $namespace = null, array $namespaceUses = [])
106
    {
107
        $this->lines = array_values($lines);
133✔
108
        $this->namespace = $namespace;
133✔
109
        $this->namespaceUses = $namespaceUses;
133✔
110

111
        $this->start = array_key_first($lines);
133✔
112
        $this->end = array_key_last($lines);
133✔
113
    }
114

115
    /**
116
     * Get the string representation of object.
117
     */
118
    public function __toString(): string
119
    {
120
        return $this->getContent();
5✔
121
    }
122

123
    /**
124
     * Get all the annotation tag names with types.
125
     *
126
     * @return non-empty-list<string>
127
     *
128
     * @deprecated Use `Annotation::TAGS_WITH_TYPES` constant instead
129
     *
130
     * @TODO 4.0 remove me
131
     */
132
    public static function getTagsWithTypes(): array
133
    {
134
        return self::TAGS_WITH_TYPES;
1✔
135
    }
136

137
    /**
138
     * Get the start position of this annotation.
139
     */
140
    public function getStart(): int
141
    {
142
        return $this->start;
5✔
143
    }
144

145
    /**
146
     * Get the end position of this annotation.
147
     */
148
    public function getEnd(): int
149
    {
150
        return $this->end;
33✔
151
    }
152

153
    /**
154
     * Get the associated tag.
155
     */
156
    public function getTag(): Tag
157
    {
158
        if (null === $this->tag) {
106✔
159
            $this->tag = new Tag($this->lines[0]);
106✔
160
        }
161

162
        return $this->tag;
106✔
163
    }
164

165
    /**
166
     * @internal
167
     */
168
    public function getTypeExpression(): ?TypeExpression
169
    {
170
        $typesContent = $this->getTypesContent();
75✔
171

172
        return null === $typesContent
74✔
UNCOV
173
            ? null
×
174
            : new TypeExpression($typesContent, $this->namespace, $this->namespaceUses);
74✔
175
    }
176

177
    /**
178
     * @internal
179
     */
180
    public function getVariableName(): ?string
181
    {
182
        $type = preg_quote($this->getTypesContent() ?? '', '/');
25✔
183
        $regex = \sprintf(
25✔
184
            '/@%s\s+(%s\s*)?(&\s*)?(\.{3}\s*)?(?<variable>\$%s)(?:.*|$)/',
25✔
185
            $this->tag->getName(),
25✔
186
            $type,
25✔
187
            TypeExpression::REGEX_IDENTIFIER,
25✔
188
        );
25✔
189

190
        if (Preg::match($regex, $this->getContent(), $matches)) {
25✔
191
            \assert(isset($matches['variable']));
23✔
192

193
            return $matches['variable'];
23✔
194
        }
195

196
        return null;
2✔
197
    }
198

199
    /**
200
     * Get the types associated with this annotation.
201
     *
202
     * @return list<string>
203
     */
204
    public function getTypes(): array
205
    {
206
        if (null === $this->types) {
59✔
207
            $typeExpression = $this->getTypeExpression();
59✔
208
            $this->types = null === $typeExpression
58✔
UNCOV
209
                ? []
×
210
                : $typeExpression->getTypes();
58✔
211
        }
212

213
        return $this->types;
58✔
214
    }
215

216
    /**
217
     * Set the types associated with this annotation.
218
     *
219
     * @param list<string> $types
220
     */
221
    public function setTypes(array $types): void
222
    {
223
        $origTypesContent = $this->getTypesContent();
8✔
224
        $newTypesContent = implode(
7✔
225
            // Fallback to union type is provided for backward compatibility (previously glue was set to `|` by default even when type was not composite)
226
            // @TODO Better handling for cases where type is fixed (original type is not composite, but was made composite during fix)
227
            $this->getTypeExpression()->getTypesGlue() ?? '|',
7✔
228
            $types,
7✔
229
        );
7✔
230

231
        if ($origTypesContent === $newTypesContent) {
7✔
UNCOV
232
            return;
×
233
        }
234

235
        $originalTypesLines = Preg::split('/([^\n\r]+\R*)/', $origTypesContent, -1, \PREG_SPLIT_NO_EMPTY | \PREG_SPLIT_DELIM_CAPTURE);
7✔
236
        $newTypesLines = Preg::split('/([^\n\r]+\R*)/', $newTypesContent, -1, \PREG_SPLIT_NO_EMPTY | \PREG_SPLIT_DELIM_CAPTURE);
7✔
237

238
        \assert(\count($originalTypesLines) === \count($newTypesLines));
7✔
239

240
        foreach ($newTypesLines as $index => $line) {
7✔
241
            \assert(isset($originalTypesLines[$index]));
7✔
242
            $pattern = '/'.preg_quote($originalTypesLines[$index], '/').'/';
7✔
243

244
            \assert(isset($this->lines[$index]));
7✔
245
            $this->lines[$index]->setContent(Preg::replace($pattern, $line, $this->lines[$index]->getContent(), 1));
7✔
246
        }
247

248
        $this->clearCache();
7✔
249
    }
250

251
    /**
252
     * Get the normalized types associated with this annotation, so they can easily be compared.
253
     *
254
     * @return list<string>
255
     */
256
    public function getNormalizedTypes(): array
257
    {
258
        $typeExpression = $this->getTypeExpression();
13✔
259
        if (null === $typeExpression) {
13✔
UNCOV
260
            return [];
×
261
        }
262

263
        $normalizedTypeExpression = $typeExpression
13✔
264
            ->mapTypes(static fn (TypeExpression $v) => new TypeExpression(strtolower($v->toString()), null, []))
13✔
265
            ->sortTypes(static fn (TypeExpression $a, TypeExpression $b) => $a->toString() <=> $b->toString())
13✔
266
        ;
13✔
267

268
        return $normalizedTypeExpression->getTypes();
13✔
269
    }
270

271
    /**
272
     * Remove this annotation by removing all its lines.
273
     */
274
    public function remove(): void
275
    {
276
        foreach ($this->lines as $line) {
12✔
277
            if ($line->isTheStart() && $line->isTheEnd()) {
12✔
278
                // Single line doc block, remove entirely
279
                $line->remove();
3✔
280
            } elseif ($line->isTheStart()) {
9✔
281
                // Multi line doc block, but start is on the same line as the first annotation, keep only the start
282
                $content = Preg::replace('#(\s*/\*\*).*#', '$1', $line->getContent());
2✔
283

284
                $line->setContent($content);
2✔
285
            } elseif ($line->isTheEnd()) {
7✔
286
                // Multi line doc block, but end is on the same line as the last annotation, keep only the end
287
                $content = Preg::replace('#(\s*)\S.*(\*/.*)#', '$1$2', $line->getContent());
2✔
288

289
                $line->setContent($content);
2✔
290
            } else {
291
                // Multi line doc block, neither start nor end on this line, can be removed safely
292
                $line->remove();
5✔
293
            }
294
        }
295

296
        $this->clearCache();
12✔
297
    }
298

299
    /**
300
     * Get the annotation content.
301
     */
302
    public function getContent(): string
303
    {
304
        return implode('', $this->lines);
109✔
305
    }
306

307
    public function supportTypes(): bool
308
    {
309
        return \in_array($this->getTag()->getName(), self::TAGS_WITH_TYPES, true);
101✔
310
    }
311

312
    /**
313
     * Get the current types content.
314
     *
315
     * Be careful modifying the underlying line as that won't flush the cache.
316
     */
317
    private function getTypesContent(): ?string
318
    {
319
        if (null === $this->typesContent) {
101✔
320
            $name = $this->getTag()->getName();
101✔
321

322
            if (!$this->supportTypes()) {
101✔
323
                throw new \RuntimeException('This tag does not support types.');
2✔
324
            }
325

326
            if (Preg::match(
99✔
327
                '{^(?:\h*\*|/\*\*)[\h*]*@'.$name.'\h+'.TypeExpression::REGEX_TYPES.'(?:(?:[*\h\v]|\&?[\.\$\s]).*)?\r?$}is',
99✔
328
                $this->getContent(),
99✔
329
                $matches,
99✔
330
            )) {
99✔
331
                \assert(isset($matches['types']));
96✔
332
                $this->typesContent = $matches['types'];
96✔
333
            }
334
        }
335

336
        return $this->typesContent;
99✔
337
    }
338

339
    private function clearCache(): void
340
    {
341
        $this->types = null;
19✔
342
        $this->typesContent = null;
19✔
343
    }
344
}
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