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

keradus / PHP-CS-Fixer / 17194063523

24 Aug 2025 09:34PM UTC coverage: 94.745% (-0.01%) from 94.756%
17194063523

push

github

keradus
update allow_hidden_params - unify towards future default

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

8 existing lines in 3 files now uncovered.

28325 of 29896 relevant lines covered (94.75%)

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

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

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

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

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

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

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

87
    private ?NamespaceAnalysis $namespace = null;
88

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

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

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

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

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

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

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

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

158
        return $this->tag;
106✔
159
    }
160

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

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

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

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

189
            return $matches['variable'];
23✔
190
        }
191

192
        return null;
2✔
193
    }
194

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

209
        return $this->types;
58✔
210
    }
211

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

227
        if ($origTypesContent === $newTypesContent) {
7✔
UNCOV
228
            return;
×
229
        }
230

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

234
        \assert(\count($originalTypesLines) === \count($newTypesLines));
7✔
235

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

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

244
        $this->clearCache();
7✔
245
    }
246

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

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

264
        return $normalizedTypeExpression->getTypes();
13✔
265
    }
266

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

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

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

292
        $this->clearCache();
12✔
293
    }
294

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

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

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

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

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

332
        return $this->typesContent;
99✔
333
    }
334

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