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

voku / Simple-PHP-Code-Parser / 24273863088

11 Apr 2026 03:41AM UTC coverage: 82.09% (-1.2%) from 83.27%
24273863088

Pull #78

github

web-flow
Merge 2bfbaf253 into cc07ae5a0
Pull Request #78: Update dependencies: php-parser v5, phpdoc-parser v2, reflection-docblock v6, type-resolver v2, phpunit v11

41 of 49 new or added lines in 9 files covered. (83.67%)

24 existing lines in 2 files now uncovered.

1540 of 1876 relevant lines covered (82.09%)

44.78 hits per line

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

85.8
/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;
105✔
58
        if ($parameterVar instanceof \PhpParser\Node\Expr\Error) {
105✔
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 : '';
105✔
67

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

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

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

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

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

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

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

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

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

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

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

118
        return $this;
105✔
119
    }
120

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

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

136
        $fileTmp = $method->getFileName();
99✔
137
        if ($fileTmp !== false) {
99✔
138
            $this->file = $fileTmp;
87✔
139
        }
140

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

152
        $docComment = $method->getDocComment();
99✔
153
        if ($docComment) {
99✔
154
            if (\stripos($docComment, '@inheritdoc') !== false) {
51✔
155
                $this->is_inheritdoc = true;
31✔
156
            }
157

158
            $this->readPhpDoc($docComment, $this->name);
51✔
159
        }
160

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

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

199
        $this->is_vararg = $parameter->isVariadic();
99✔
200

201
        $this->is_passed_by_ref = $parameter->isPassedByReference();
99✔
202

203
        // Extract PHP 8.0+ attributes
204
        $this->attributes = Utils::extractAttributesFromReflection($parameter);
99✔
205

206
        return $this;
99✔
207
    }
208

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

218
        if ($this->type) {
×
219
            return $this->type;
×
220
        }
221

222
        if ($this->typeFromPhpDocSimple) {
×
223
            return $this->typeFromPhpDocSimple;
×
224
        }
225

226
        return null;
×
227
    }
228

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

243
        try {
244
            $phpDoc = DocFactoryProvider::getDocFactory()->create($docComment);
63✔
245

246
            $parsedParamTags = $phpDoc->getTagsByName('param');
63✔
247

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

256
                        $type = $parsedParamTag->getType();
59✔
257

258
                        $this->typeFromPhpDoc = Utils::normalizePhpType($type . '');
59✔
259

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

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

275
                    $parsedParamTagParam = (string) $parsedParamTag;
59✔
276
                    $spitedData = Utils::splitTypeAndVariable($parsedParamTag);
59✔
277
                    $variableName = $spitedData['variableName'];
59✔
278

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

285
                    break;
59✔
286
                }
287
            }
288

289
            $parsedParamTags = $phpDoc->getTagsByName('psalm-param')
63✔
290
                               + $phpDoc->getTagsByName('phpstan-param');
63✔
291

292
            if (!empty($parsedParamTags)) {
63✔
293
                foreach ($parsedParamTags as $parsedParamTag) {
63✔
294
                    if (!$parsedParamTag instanceof \phpDocumentor\Reflection\DocBlock\Tags\Generic) {
32✔
295
                        continue;
×
296
                    }
297

298
                    $spitedData = Utils::splitTypeAndVariable($parsedParamTag);
32✔
299
                    $parsedParamTagStr = $spitedData['parsedParamTagStr'];
32✔
300
                    $variableName = $spitedData['variableName'];
32✔
301

302
                    // check only the current "param"-tag
303
                    if (!$variableName || \strtoupper($parameterName) !== \strtoupper($variableName)) {
32✔
304
                        continue;
28✔
305
                    }
306

307
                    $this->typeFromPhpDocExtended = Utils::modernPhpdoc($parsedParamTagStr);
32✔
308
                }
309
            }
310
        } catch (\Exception $e) {
12✔
311
            $this->addParseError($e);
12✔
312
        }
313

314
        try {
315
            $this->readPhpDocByTokens($docComment, $parameterName);
63✔
316
        } catch (\Exception $e) {
12✔
317
            $this->addParseError($e);
12✔
318
        }
319

320
        $this->reportBrokenParamTagWithoutType($docComment, $parameterName);
63✔
321
    }
322

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

330
        $paramContent = null;
63✔
331
        foreach ($tokens->getTokens() as $token) {
63✔
332
            $content = $token[0];
63✔
333

334
            if ($content === '@param' || $content === '@psalm-param' || $content === '@phpstan-param') {
63✔
335
                // reset
336
                $paramContent = '';
59✔
337

338
                continue;
59✔
339
            }
340

341
            // We can stop if we found the param variable e.g. `@param array{foo:int} $param`.
342
            if ($content === '$' . $parameterName) {
63✔
343
                break;
59✔
344
            }
345

346
            if ($paramContent !== null) {
63✔
347
                $paramContent .= $content;
59✔
348
            }
349
        }
350

351
        $paramContent = $paramContent ? \trim($paramContent) : null;
63✔
352
        if ($paramContent) {
63✔
353
            if (!$this->phpDocRaw) {
59✔
354
                $this->phpDocRaw = $paramContent . ' ' . '$' . $parameterName;
32✔
355
            }
356
            $this->typeFromPhpDocExtended = Utils::modernPhpdoc($paramContent);
59✔
357
        }
358
    }
359

360
    private function reportBrokenParamTagWithoutType(string $docComment, string $parameterName): void
361
    {
362
        if ($this->line === null) {
63✔
NEW
363
            return;
×
364
        }
365

366
        if (!\preg_match('/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$/u', $parameterName)) {
63✔
NEW
367
            return;
×
368
        }
369

370
        if (
371
            !\preg_match(
63✔
372
                '#@(param|psalm-param|phpstan-param)[ \t]+\$' . $parameterName . '(?=[ \t\r\n\*]|$)#u',
63✔
373
                $docComment
63✔
374
            )
63✔
375
        ) {
376
            return;
63✔
377
        }
378

379
        try {
380
            // Re-parse the malformed tag payload to preserve the original parser
381
            // error message even though the docblock library now falls back to mixed.
382
            Utils::modernPhpdoc('$' . $parameterName);
36✔
383
        } catch (\Exception $e) {
36✔
384
            $this->addParseError($e);
36✔
385
        }
386
    }
387

388
    private function addParseError(\Exception $e): void
389
    {
390
        $tmpErrorMessage = $this->name . ':' . ($this->line ?? '?') . ' | ' . \print_r($e->getMessage(), true);
44✔
391
        $this->parseError[\md5($tmpErrorMessage)] = $tmpErrorMessage;
44✔
392
    }
393
}
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