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

keradus / PHP-CS-Fixer / 17252691116

26 Aug 2025 11:09PM UTC coverage: 94.743% (-0.01%) from 94.755%
17252691116

push

github

keradus
chore: apply phpdoc_tag_no_named_arguments

28313 of 29884 relevant lines covered (94.74%)

45.64 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
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-type',
43
        'phpstan-import-type',
44
        'property',
45
        'property-read',
46
        'property-write',
47
        'psalm-type',
48
        'psalm-import-type',
49
        'return',
50
        'throws',
51
        'type',
52
        'var',
53
    ];
54

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

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

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

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

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

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

89
    private ?NamespaceAnalysis $namespace = null;
90

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

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

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

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

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

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

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

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

160
        return $this->tag;
106✔
161
    }
162

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

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

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

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

191
            return $matches['variable'];
23✔
192
        }
193

194
        return null;
2✔
195
    }
196

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

211
        return $this->types;
58✔
212
    }
213

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

229
        if ($origTypesContent === $newTypesContent) {
7✔
230
            return;
×
231
        }
232

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

236
        \assert(\count($originalTypesLines) === \count($newTypesLines));
7✔
237

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

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

246
        $this->clearCache();
7✔
247
    }
248

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

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

266
        return $normalizedTypeExpression->getTypes();
13✔
267
    }
268

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

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

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

294
        $this->clearCache();
12✔
295
    }
296

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

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

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

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

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

334
        return $this->typesContent;
99✔
335
    }
336

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