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

voku / Simple-PHP-Code-Parser / 24288682424

11 Apr 2026 06:21PM UTC coverage: 82.653% (-0.2%) from 82.886%
24288682424

Pull #83

github

web-flow
Merge 00ed1eaa9 into 90e1e60d3
Pull Request #83: Fix CI pipeline: phpunit.xml validation warning, php-parser v4 test skip, and comprehensive type-analysis regression coverage

268 of 295 new or added lines in 7 files covered. (90.85%)

30 existing lines in 3 files now uncovered.

1720 of 2081 relevant lines covered (82.65%)

28.67 hits per line

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

86.98
/src/voku/SimplePhpParser/Model/PHPParameter.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace voku\SimplePhpParser\Model;
6

7
use PhpParser\Comment\Doc;
8
use PhpParser\Node\FunctionLike;
9
use PhpParser\Node\Param;
10
use ReflectionParameter;
11
use voku\SimplePhpParser\Parsers\Helper\DocFactoryProvider;
12
use voku\SimplePhpParser\Parsers\Helper\Utils;
13

14
class PHPParameter extends BasePHPElement
15
{
16
    /**
17
     * @var mixed|null
18
     */
19
    public $defaultValue;
20

21
    public ?string $phpDocRaw = null;
22

23
    public ?string $type = null;
24

25
    public ?string $typeFromDefaultValue = null;
26

27
    public ?string $typeFromPhpDoc = null;
28

29
    public ?string $typeFromPhpDocSimple = null;
30

31
    public ?string $typeFromPhpDocExtended = null;
32

33
    public ?string $typeFromPhpDocMaybeWithComment = null;
34

35
    public ?bool $is_vararg = null;
36

37
    public ?bool $is_passed_by_ref = null;
38

39
    public ?bool $is_inheritdoc = null;
40

41
    /**
42
     * PHP 8.0+ attributes on this parameter.
43
     *
44
     * @var PHPAttribute[]
45
     */
46
    public array $attributes = [];
47

48
    /**
49
     * @param Param        $parameter
50
     * @param FunctionLike $node
51
     * @param mixed|null   $classStr
52
     *
53
     * @return $this
54
     */
55
    public function readObjectFromPhpNode($parameter, $node = null, $classStr = null): self
56
    {
57
        $parameterVar = $parameter->var;
75✔
58
        if ($parameterVar instanceof \PhpParser\Node\Expr\Error) {
75✔
59
            $this->parseError[] = ($this->line ?? '?') . ':' . ($this->pos ?? '') . ' | may be at this position an expression is required';
×
60

61
            $this->name = \md5(\uniqid('error', true));
×
62

63
            return $this;
×
64
        }
65

66
        $this->name = \is_string($parameterVar->name) ? $parameterVar->name : '';
75✔
67

68
        if ($node) {
75✔
69
            $this->prepareNode($node);
75✔
70

71
            $docComment = $node->getDocComment();
75✔
72
            if ($docComment) {
75✔
73
                $docCommentText = $docComment->getText();
40✔
74

75
                if (\stripos($docCommentText, '@inheritdoc') !== false) {
40✔
76
                    $this->is_inheritdoc = true;
18✔
77
                }
78

79
                $this->readPhpDoc($docComment, $this->name);
40✔
80
            }
81
        }
82

83
        if ($parameter->type !== null) {
75✔
84
            if (!$this->type) {
55✔
85
                $typeStr = Utils::typeNodeToString($parameter->type);
21✔
86
                if ($typeStr !== null) {
21✔
87
                    $this->type = $typeStr;
21✔
88
                }
89
            }
90

91
            if ($parameter->type instanceof \PhpParser\Node\NullableType) {
55✔
92
                if ($this->type && $this->type !== 'null' && \strpos($this->type, 'null|') !== 0) {
13✔
93
                    $this->type = 'null|' . $this->type;
8✔
94
                } elseif (!$this->type) {
11✔
95
                    $this->type = 'null|mixed';
×
96
                }
97
            }
98
        }
99

100
        if ($parameter->default) {
75✔
101
            $defaultValue = Utils::getPhpParserValueFromNode($parameter->default, $classStr, $this->parserContainer);
33✔
102
            if ($defaultValue !== Utils::GET_PHP_PARSER_VALUE_FROM_NODE_HELPER) {
33✔
103
                $this->defaultValue = $defaultValue;
33✔
104

105
                $this->typeFromDefaultValue = Utils::normalizePhpType(\gettype($this->defaultValue));
33✔
106
            }
107
        }
108

109
        $this->is_vararg = $parameter->variadic;
75✔
110

111
        $this->is_passed_by_ref = $parameter->byRef;
75✔
112

113
        // Extract PHP 8.0+ attributes (only if not already populated by reflection)
114
        if (empty($this->attributes) && !empty($parameter->attrGroups)) {
75✔
115
            $this->attributes = Utils::extractAttributesFromAstNode($parameter->attrGroups);
7✔
116
        }
117

118
        return $this;
75✔
119
    }
120

121
    /**
122
     * @param ReflectionParameter $parameter
123
     *
124
     * @return $this
125
     */
126
    public function readObjectFromReflection($parameter): self
127
    {
128
        $this->name = $parameter->getName();
56✔
129

130
        $method = $parameter->getDeclaringFunction();
56✔
131
        if (!$this->line) {
56✔
132
            $lineTmp = $method->getStartLine();
56✔
133
            if ($lineTmp !== false) {
56✔
134
                $this->line = $lineTmp;
50✔
135
            }
136
        }
137

138
        $fileTmp = $method->getFileName();
56✔
139
        if ($fileTmp !== false) {
56✔
140
            $this->file = $fileTmp;
50✔
141
        }
142

143
        if ($parameter->isDefaultValueAvailable()) {
56✔
144
            try {
145
                $this->defaultValue = $parameter->getDefaultValue();
28✔
146
            } catch (\ReflectionException $e) {
×
147
                // nothing
148
            }
149
            if ($this->defaultValue !== null) {
28✔
150
                $this->typeFromDefaultValue = Utils::normalizePhpType(\gettype($this->defaultValue));
28✔
151
            }
152
        }
153

154
        $docComment = $method->getDocComment();
56✔
155
        if ($docComment) {
56✔
156
            if (\stripos($docComment, '@inheritdoc') !== false) {
28✔
157
                $this->is_inheritdoc = true;
18✔
158
            }
159

160
            $this->readPhpDoc($docComment, $this->name);
28✔
161
        }
162

163
        try {
164
            $type = $parameter->getType();
56✔
165
        } catch (\ReflectionException $e) {
×
166
            $type = null;
×
167
        }
168
        if ($type !== null) {
56✔
169
            if (\method_exists($type, 'getName')) {
56✔
170
                $this->type = Utils::normalizePhpType($type->getName(), true);
44✔
171
            } else {
172
                $this->type = Utils::normalizePhpType($type . '', true);
22✔
173
            }
174
            if ($this->type && \class_exists($this->type, true)) {
56✔
175
                $this->type = '\\' . \ltrim($this->type, '\\');
30✔
176
            }
177

178
            try {
179
                $constNameTmp = $parameter->getDefaultValueConstantName();
56✔
180
                if ($constNameTmp && \defined($constNameTmp)) {
28✔
181
                    $defaultTmp = \constant($constNameTmp);
6✔
182
                    if ($defaultTmp === null) {
6✔
183
                        if ($this->type && $this->type !== 'null' && \strpos($this->type, 'null|') !== 0) {
×
184
                            $this->type = 'null|' . $this->type;
×
185
                        } elseif (!$this->type) {
×
186
                            $this->type = 'null|mixed';
28✔
187
                        }
188
                    }
189
                }
190
            } catch (\ReflectionException $e) {
54✔
191
                if ($type->allowsNull()) {
54✔
192
                    if ($this->type && $this->type !== 'null' && \strpos($this->type, 'null|') !== 0) {
8✔
193
                        $this->type = 'null|' . $this->type;
8✔
194
                    } elseif (!$this->type) {
×
195
                        $this->type = 'null|mixed';
×
196
                    }
197
                }
198
            }
199
        }
200

201
        $this->is_vararg = $parameter->isVariadic();
56✔
202

203
        $this->is_passed_by_ref = $parameter->isPassedByReference();
56✔
204

205
        // Extract PHP 8.0+ attributes
206
        $this->attributes = Utils::extractAttributesFromReflection($parameter);
56✔
207

208
        return $this;
56✔
209
    }
210

211
    /**
212
     * @return string|null
213
     */
214
    public function getType(): ?string
215
    {
216
        if ($this->typeFromPhpDocExtended) {
×
217
            return $this->typeFromPhpDocExtended;
×
218
        }
219

220
        if ($this->type) {
×
221
            return $this->type;
×
222
        }
223

224
        if ($this->typeFromPhpDocSimple) {
×
225
            return $this->typeFromPhpDocSimple;
×
226
        }
227

228
        return null;
×
229
    }
230

231
    /**
232
     * @param Doc|string $doc
233
     */
234
    private function readPhpDoc($doc, string $parameterName): void
235
    {
236
        if ($doc instanceof Doc) {
42✔
237
            $docComment = $doc->getText();
40✔
238
        } else {
239
            $docComment = $doc;
28✔
240
        }
241
        if ($docComment === '') {
42✔
242
            return;
×
243
        }
244

245
        try {
246
            $phpDoc = DocFactoryProvider::getDocFactory()->create($docComment);
42✔
247

248
            $parsedParamTags = $phpDoc->getTagsByName('param');
42✔
249

250
            if (!empty($parsedParamTags)) {
42✔
251
                foreach ($parsedParamTags as $parsedParamTag) {
38✔
252
                    if ($parsedParamTag instanceof \phpDocumentor\Reflection\DocBlock\Tags\Param) {
38✔
253
                        // check only the current "param"-tag
254
                        if (\strtoupper($parameterName) !== \strtoupper((string) $parsedParamTag->getVariableName())) {
38✔
255
                            continue;
28✔
256
                        }
257

258
                        $type = $parsedParamTag->getType();
38✔
259

260
                        $this->typeFromPhpDoc = Utils::normalizePhpType($type . '');
38✔
261

262
                        $typeFromPhpDocMaybeWithCommentTmp = \trim((string) $parsedParamTag);
38✔
263
                        if (
264
                            $typeFromPhpDocMaybeWithCommentTmp
38✔
265
                            &&
266
                            \strpos($typeFromPhpDocMaybeWithCommentTmp, '$') !== 0
38✔
267
                        ) {
268
                            $this->typeFromPhpDocMaybeWithComment = $typeFromPhpDocMaybeWithCommentTmp;
38✔
269
                        }
270

271
                        $typeTmp = Utils::parseDocTypeObject($type);
38✔
272
                        if ($typeTmp !== '') {
38✔
273
                            $this->typeFromPhpDocSimple = $typeTmp;
38✔
274
                        }
275
                    }
276

277
                    $parsedParamTagParam = (string) $parsedParamTag;
38✔
278
                    $spitedData = Utils::splitTypeAndVariable($parsedParamTag);
38✔
279
                    $variableName = $spitedData['variableName'];
38✔
280

281
                    // check only the current "param"-tag
282
                    if ($variableName && \strtoupper($parameterName) === \strtoupper($variableName)) {
38✔
283
                        $this->phpDocRaw = $parsedParamTagParam;
38✔
284
                        $this->typeFromPhpDocExtended = Utils::modernPhpdoc($parsedParamTagParam);
38✔
285
                    }
286

287
                    break;
38✔
288
                }
289
            }
290

291
            $parsedParamTags = $phpDoc->getTagsByName('psalm-param')
42✔
292
                               + $phpDoc->getTagsByName('phpstan-param');
42✔
293

294
            if (!empty($parsedParamTags)) {
42✔
295
                foreach ($parsedParamTags as $parsedParamTag) {
42✔
296
                    if (!$parsedParamTag instanceof \phpDocumentor\Reflection\DocBlock\Tags\Generic) {
22✔
297
                        continue;
×
298
                    }
299

300
                    $spitedData = Utils::splitTypeAndVariable($parsedParamTag);
22✔
301
                    $parsedParamTagStr = $spitedData['parsedParamTagStr'];
22✔
302
                    $variableName = $spitedData['variableName'];
22✔
303

304
                    // check only the current "param"-tag
305
                    if (!$variableName || \strtoupper($parameterName) !== \strtoupper($variableName)) {
22✔
306
                        continue;
14✔
307
                    }
308

309
                    $this->typeFromPhpDocExtended = Utils::modernPhpdoc($parsedParamTagStr);
22✔
310
                }
311
            }
312
        } catch (\Exception $e) {
8✔
313
            $this->addParseError($e);
8✔
314
        }
315

316
        try {
317
            $this->readPhpDocByTokens($docComment, $parameterName);
42✔
UNCOV
318
        } catch (\Exception $e) {
×
UNCOV
319
            $this->addParseError($e);
×
320
        }
321

322
        $this->reportBrokenParamTagWithoutType($docComment, $parameterName);
42✔
323
    }
324

325
    /**
326
     * @throws \PHPStan\PhpDocParser\Parser\ParserException
327
     */
328
    private function readPhpDocByTokens(string $docComment, string $parameterName): void
329
    {
330
        $tokens = Utils::modernPhpdocTokens($docComment);
42✔
331

332
        // Track standard (@param) and extended (@phpstan-param / @psalm-param) content separately
333
        // so that the more specific phpstan/psalm annotation always wins regardless of tag order.
334
        // We scan the ENTIRE docblock to find all occurrences of both tag types for this parameter.
335
        $paramContent = null;
42✔
336
        $extendedParamContent = null;
42✔
337
        $currentTarget = null; // 'standard' | 'extended'
42✔
338
        $currentContent = '';
42✔
339

340
        foreach ($tokens->getTokens() as $token) {
42✔
341
            $content = $token[0];
42✔
342

343
            if ($content === '@param') {
42✔
344
                $currentTarget = 'standard';
38✔
345
                $currentContent = '';
38✔
346
                continue;
38✔
347
            }
348

349
            if ($content === '@psalm-param' || $content === '@phpstan-param') {
42✔
350
                $currentTarget = 'extended';
22✔
351
                $currentContent = '';
22✔
352
                continue;
22✔
353
            }
354

355
            if ($currentTarget !== null) {
42✔
356
                // Check if we hit the target parameter variable e.g. `$param`.
357
                if ($content === '$' . $parameterName) {
40✔
358
                    if ($currentTarget === 'standard') {
40✔
359
                        $paramContent = \trim($currentContent);
38✔
360
                    } else {
361
                        $extendedParamContent = \trim($currentContent);
22✔
362
                    }
363
                    $currentTarget = null;
40✔
364
                    $currentContent = '';
40✔
365
                    continue;
40✔
366
                }
367

368
                // Check if we hit a different parameter variable — discard this tag.
369
                if (\strlen($content) > 1 && $content[0] === '$') {
40✔
370
                    $currentTarget = null;
28✔
371
                    $currentContent = '';
28✔
372
                    continue;
28✔
373
                }
374

375
                $currentContent .= $content;
40✔
376
            }
377
        }
378

379
        // Prefer @phpstan-param / @psalm-param over plain @param regardless of tag order.
380
        $bestContent = null;
42✔
381
        if ($extendedParamContent !== null && $extendedParamContent !== '') {
42✔
382
            $bestContent = $extendedParamContent;
20✔
383
        } elseif ($paramContent !== null && $paramContent !== '') {
36✔
384
            $bestContent = $paramContent;
34✔
385
        }
386

387
        if ($bestContent) {
42✔
388
            if (!$this->phpDocRaw) {
40✔
389
                $this->phpDocRaw = $bestContent . ' ' . '$' . $parameterName;
16✔
390
            }
391
            try {
392
                $this->typeFromPhpDocExtended = Utils::modernPhpdoc($bestContent);
40✔
393
            } catch (\PHPStan\PhpDocParser\Parser\ParserException $e) {
8✔
394
                $recoveredType = Utils::recoverBrokenPhpdocType($bestContent);
8✔
395
                if ($recoveredType !== null) {
8✔
396
                    $normalizedRecoveredType = Utils::normalizePhpType($recoveredType);
8✔
397
                    $this->typeFromPhpDoc = $this->typeFromPhpDoc ?? $normalizedRecoveredType;
8✔
398
                    $this->typeFromPhpDocSimple = $this->typeFromPhpDocSimple ?? $normalizedRecoveredType;
8✔
399
                    $this->typeFromPhpDocExtended = $recoveredType;
8✔
400
                }
401

402
                $this->addParseError($e);
8✔
403
            }
404
        }
405
    }
406

407
    private function reportBrokenParamTagWithoutType(string $docComment, string $parameterName): void
408
    {
409
        if ($this->line === null) {
42✔
410
            return;
×
411
        }
412

413
        if (!\preg_match('/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$/u', $parameterName)) {
42✔
414
            return;
×
415
        }
416

417
        if (
418
            !\preg_match(
42✔
419
                '#@(param|psalm-param|phpstan-param)[ \t]+\$' . $parameterName . '(?=[ \t\r\n\*]|$)#u',
42✔
420
                $docComment
42✔
421
            )
42✔
422
        ) {
423
            return;
42✔
424
        }
425

426
        try {
427
            // Re-parse the malformed tag payload to preserve the original parser
428
            // error message even though the docblock library now falls back to mixed.
429
            Utils::modernPhpdoc('$' . $parameterName);
18✔
430
        } catch (\Exception $e) {
18✔
431
            $this->addParseError($e);
18✔
432
        }
433
    }
434

435
    private function addParseError(\Exception $e): void
436
    {
437
        $tmpErrorMessage = $this->name . ':' . ($this->line ?? '?') . ' | ' . \print_r($e->getMessage(), true);
24✔
438
        $this->parseError[\md5($tmpErrorMessage)] = $tmpErrorMessage;
24✔
439
    }
440
}
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