• 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

94.87
/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
final class Annotation
28
{
29
    /**
30
     * All the annotation tag names with types.
31
     *
32
     * @var list<string>
33
     */
34
    private const TAGS = [
35
        'extends',
36
        'implements',
37
        'method',
38
        'param',
39
        'param-out',
40
        'property',
41
        'property-read',
42
        'property-write',
43
        'return',
44
        'throws',
45
        'type',
46
        'var',
47
    ];
48

49
    /**
50
     * The lines that make up the annotation.
51
     *
52
     * @var array<int, Line>
53
     */
54
    private array $lines;
55

56
    /**
57
     * The position of the first line of the annotation in the docblock.
58
     */
59
    private int $start;
60

61
    /**
62
     * The position of the last line of the annotation in the docblock.
63
     */
64
    private int $end;
65

66
    /**
67
     * The associated tag.
68
     */
69
    private ?Tag $tag = null;
70

71
    /**
72
     * Lazy loaded, cached types content.
73
     */
74
    private ?string $typesContent = null;
75

76
    /**
77
     * The cached types.
78
     *
79
     * @var null|list<string>
80
     */
81
    private ?array $types = null;
82

83
    private ?NamespaceAnalysis $namespace = null;
84

85
    /**
86
     * @var list<NamespaceUseAnalysis>
87
     */
88
    private array $namespaceUses;
89

90
    /**
91
     * Create a new line instance.
92
     *
93
     * @param array<int, Line>           $lines
94
     * @param null|NamespaceAnalysis     $namespace
95
     * @param list<NamespaceUseAnalysis> $namespaceUses
96
     */
97
    public function __construct(array $lines, $namespace = null, array $namespaceUses = [])
98
    {
99
        $this->lines = array_values($lines);
128✔
100
        $this->namespace = $namespace;
128✔
101
        $this->namespaceUses = $namespaceUses;
128✔
102

103
        $this->start = array_key_first($lines);
128✔
104
        $this->end = array_key_last($lines);
128✔
105
    }
106

107
    /**
108
     * Get the string representation of object.
109
     */
110
    public function __toString(): string
111
    {
112
        return $this->getContent();
5✔
113
    }
114

115
    /**
116
     * Get all the annotation tag names with types.
117
     *
118
     * @return list<string>
119
     */
120
    public static function getTagsWithTypes(): array
121
    {
122
        return self::TAGS;
1✔
123
    }
124

125
    /**
126
     * Get the start position of this annotation.
127
     */
128
    public function getStart(): int
129
    {
130
        return $this->start;
5✔
131
    }
132

133
    /**
134
     * Get the end position of this annotation.
135
     */
136
    public function getEnd(): int
137
    {
138
        return $this->end;
32✔
139
    }
140

141
    /**
142
     * Get the associated tag.
143
     */
144
    public function getTag(): Tag
145
    {
146
        if (null === $this->tag) {
101✔
147
            $this->tag = new Tag($this->lines[0]);
101✔
148
        }
149

150
        return $this->tag;
101✔
151
    }
152

153
    /**
154
     * @internal
155
     */
156
    public function getTypeExpression(): ?TypeExpression
157
    {
158
        $typesContent = $this->getTypesContent();
74✔
159

160
        return null === $typesContent
73✔
161
            ? null
×
162
            : new TypeExpression($typesContent, $this->namespace, $this->namespaceUses);
73✔
163
    }
164

165
    /**
166
     * @internal
167
     */
168
    public function getVariableName(): ?string
169
    {
170
        $type = preg_quote($this->getTypesContent() ?? '', '/');
21✔
171
        $regex = \sprintf(
21✔
172
            '/@%s\s+(%s\s*)?(&\s*)?(\.{3}\s*)?(?<variable>\$%s)(?:.*|$)/',
21✔
173
            $this->tag->getName(),
21✔
174
            $type,
21✔
175
            TypeExpression::REGEX_IDENTIFIER
21✔
176
        );
21✔
177

178
        if (Preg::match($regex, $this->lines[0]->getContent(), $matches)) {
21✔
179
            return $matches['variable'];
19✔
180
        }
181

182
        return null;
2✔
183
    }
184

185
    /**
186
     * Get the types associated with this annotation.
187
     *
188
     * @return list<string>
189
     */
190
    public function getTypes(): array
191
    {
192
        if (null === $this->types) {
58✔
193
            $typeExpression = $this->getTypeExpression();
58✔
194
            $this->types = null === $typeExpression
57✔
195
                ? []
×
196
                : $typeExpression->getTypes();
57✔
197
        }
198

199
        return $this->types;
57✔
200
    }
201

202
    /**
203
     * Set the types associated with this annotation.
204
     *
205
     * @param list<string> $types
206
     */
207
    public function setTypes(array $types): void
208
    {
209
        $origTypesContent = $this->getTypesContent();
8✔
210
        $newTypesContent = implode(
7✔
211
            // Fallback to union type is provided for backward compatibility (previously glue was set to `|` by default even when type was not composite)
212
            // @TODO Better handling for cases where type is fixed (original type is not composite, but was made composite during fix)
213
            $this->getTypeExpression()->getTypesGlue() ?? '|',
7✔
214
            $types
7✔
215
        );
7✔
216

217
        if ($origTypesContent === $newTypesContent) {
7✔
218
            return;
×
219
        }
220

221
        $pattern = '/'.preg_quote($origTypesContent, '/').'/';
7✔
222

223
        $this->lines[0]->setContent(Preg::replace($pattern, $newTypesContent, $this->lines[0]->getContent(), 1));
7✔
224

225
        $this->clearCache();
7✔
226
    }
227

228
    /**
229
     * Get the normalized types associated with this annotation, so they can easily be compared.
230
     *
231
     * @return list<string>
232
     */
233
    public function getNormalizedTypes(): array
234
    {
235
        $typeExpression = $this->getTypeExpression();
13✔
236
        if (null === $typeExpression) {
13✔
237
            return [];
×
238
        }
239

240
        $normalizedTypeExpression = $typeExpression
13✔
241
            ->mapTypes(static fn (TypeExpression $v) => new TypeExpression(strtolower($v->toString()), null, []))
13✔
242
            ->sortTypes(static fn (TypeExpression $a, TypeExpression $b) => $a->toString() <=> $b->toString())
13✔
243
        ;
13✔
244

245
        return $normalizedTypeExpression->getTypes();
13✔
246
    }
247

248
    /**
249
     * Remove this annotation by removing all its lines.
250
     */
251
    public function remove(): void
252
    {
253
        foreach ($this->lines as $line) {
12✔
254
            if ($line->isTheStart() && $line->isTheEnd()) {
12✔
255
                // Single line doc block, remove entirely
256
                $line->remove();
3✔
257
            } elseif ($line->isTheStart()) {
9✔
258
                // Multi line doc block, but start is on the same line as the first annotation, keep only the start
259
                $content = Preg::replace('#(\s*/\*\*).*#', '$1', $line->getContent());
2✔
260

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

266
                $line->setContent($content);
2✔
267
            } else {
268
                // Multi line doc block, neither start nor end on this line, can be removed safely
269
                $line->remove();
5✔
270
            }
271
        }
272

273
        $this->clearCache();
12✔
274
    }
275

276
    /**
277
     * Get the annotation content.
278
     */
279
    public function getContent(): string
280
    {
281
        return implode('', $this->lines);
10✔
282
    }
283

284
    public function supportTypes(): bool
285
    {
286
        return \in_array($this->getTag()->getName(), self::TAGS, true);
96✔
287
    }
288

289
    /**
290
     * Get the current types content.
291
     *
292
     * Be careful modifying the underlying line as that won't flush the cache.
293
     */
294
    private function getTypesContent(): ?string
295
    {
296
        if (null === $this->typesContent) {
96✔
297
            $name = $this->getTag()->getName();
96✔
298

299
            if (!$this->supportTypes()) {
96✔
300
                throw new \RuntimeException('This tag does not support types.');
2✔
301
            }
302

303
            $matchingResult = Preg::match(
94✔
304
                '{^(?:\h*\*|/\*\*)[\h*]*@'.$name.'\h+'.TypeExpression::REGEX_TYPES.'(?:(?:[*\h\v]|\&?[\.\$]).*)?\r?$}is',
94✔
305
                $this->lines[0]->getContent(),
94✔
306
                $matches
94✔
307
            );
94✔
308

309
            $this->typesContent = $matchingResult
94✔
310
                ? $matches['types']
91✔
311
                : null;
3✔
312
        }
313

314
        return $this->typesContent;
94✔
315
    }
316

317
    private function clearCache(): void
318
    {
319
        $this->types = null;
19✔
320
        $this->typesContent = null;
19✔
321
    }
322
}
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