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

voku / Simple-PHP-Code-Parser / 24289924886

11 Apr 2026 07:31PM UTC coverage: 84.527% (+1.8%) from 82.757%
24289924886

push

github

web-flow
Merge pull request #83 from voku/copilot/fix-failing-phpdoc-test

Fix CI pipeline: phpunit.xml validation warning, php-parser v4 test skip, and comprehensive type-analysis regression coverage

272 of 295 new or added lines in 7 files covered. (92.2%)

5 existing lines in 2 files now uncovered.

1759 of 2081 relevant lines covered (84.53%)

113.31 hits per line

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

73.17
/src/voku/SimplePhpParser/Model/PHPFunction.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace voku\SimplePhpParser\Model;
6

7
use phpDocumentor\Reflection\DocBlock\Tags\Generic;
8
use phpDocumentor\Reflection\DocBlock\Tags\Return_;
9
use PhpParser\Comment\Doc;
10
use PhpParser\Node\Stmt\Function_;
11
use ReflectionFunction;
12
use voku\SimplePhpParser\Parsers\Helper\DocFactoryProvider;
13
use voku\SimplePhpParser\Parsers\Helper\Utils;
14

15
class PHPFunction extends BasePHPElement
16
{
17
    use PHPDocElement;
18

19
    /**
20
     * @var PHPParameter[]
21
     */
22
    public array $parameters = [];
23

24
    /**
25
     * PHP 8.0+ attributes on this function.
26
     *
27
     * @var PHPAttribute[]
28
     */
29
    public array $attributes = [];
30

31
    public ?string $returnPhpDocRaw = null;
32

33
    public ?string $returnType = null;
34

35
    public ?string $returnTypeFromPhpDoc = null;
36

37
    public ?string $returnTypeFromPhpDocSimple = null;
38

39
    public ?string $returnTypeFromPhpDocExtended = null;
40

41
    public ?string $returnTypeFromPhpDocMaybeWithComment = null;
42

43
    public string $summary = '';
44

45
    public string $description = '';
46

47
    /**
48
     * @param Function_   $node
49
     * @param string|null $dummy
50
     *
51
     * @return $this
52
     */
53
    public function readObjectFromPhpNode($node, $dummy = null): self
54
    {
55
        $this->prepareNode($node);
88✔
56

57
        $this->name = static::getFQN($node);
88✔
58

59
        // Extract PHP 8.0+ attributes
60
        if (!empty($node->attrGroups)) {
88✔
61
            $this->attributes = Utils::extractAttributesFromAstNode($node->attrGroups);
8✔
62
        }
63

64
        /** @noinspection NotOptimalIfConditionsInspection */
65
        if (\function_exists($this->name)) {
88✔
66
            $reflectionFunction = Utils::createFunctionReflectionInstance($this->name);
48✔
67
            $this->readObjectFromReflection($reflectionFunction);
48✔
68
        }
69

70
        if ($node->returnType) {
88✔
71
            if (!$this->returnType) {
16✔
72
                $returnTypeStr = Utils::typeNodeToString($node->returnType);
16✔
73
                if ($returnTypeStr !== null) {
16✔
74
                    $this->returnType = $returnTypeStr;
16✔
75
                }
76
            }
77

78
            if ($node->returnType instanceof \PhpParser\Node\NullableType) {
16✔
79
                if ($this->returnType && $this->returnType !== 'null' && \strpos($this->returnType, 'null|') !== 0) {
×
80
                    $this->returnType = 'null|' . $this->returnType;
×
81
                } elseif (!$this->returnType) {
×
82
                    $this->returnType = 'null|mixed';
×
83
                }
84
            }
85
        }
86

87
        $docComment = $node->getDocComment();
88✔
88
        if ($docComment) {
88✔
89
            try {
90
                $phpDoc = DocFactoryProvider::getDocFactory()->create($docComment->getText());
64✔
91
                $this->summary = $phpDoc->getSummary();
64✔
92
                $this->description = (string) $phpDoc->getDescription();
64✔
93
            } catch (\Exception $e) {
×
94
                $tmpErrorMessage = \sprintf(
×
95
                    '%s:%s | %s',
×
96
                    $this->name,
×
97
                    $this->line ?? '?',
×
98
                    \print_r($e->getMessage(), true)
×
99
                );
×
100
                $this->parseError[\md5($tmpErrorMessage)] = $tmpErrorMessage;
×
101
            }
102
        }
103

104
        foreach ($node->getParams() as $parameter) {
88✔
105
            $parameterVar = $parameter->var;
88✔
106
            if ($parameterVar instanceof \PhpParser\Node\Expr\Error) {
88✔
107
                $this->parseError[] = \sprintf(
×
108
                    '%s:%s | maybe at this position an expression is required',
×
109
                    $this->line ?? '?',
×
110
                    $this->pos ?? ''
×
111
                );
×
112

113
                return $this;
×
114
            }
115

116
            $paramNameTmp = $parameterVar->name;
88✔
117
            \assert(\is_string($paramNameTmp));
118

119
            if (isset($this->parameters[$paramNameTmp])) {
88✔
120
                $this->parameters[$paramNameTmp] = $this->parameters[$paramNameTmp]->readObjectFromPhpNode($parameter, $node);
48✔
121
            } else {
122
                $this->parameters[$paramNameTmp] = (new PHPParameter($this->parserContainer))->readObjectFromPhpNode($parameter, $node);
48✔
123
            }
124
        }
125

126
        $this->collectTags($node);
88✔
127

128
        $docComment = $node->getDocComment();
88✔
129
        if ($docComment) {
88✔
130
            $this->readPhpDoc($docComment);
64✔
131
        }
132

133
        return $this;
88✔
134
    }
135

136
    /**
137
     * @param ReflectionFunction $function
138
     *
139
     * @return $this
140
     */
141
    public function readObjectFromReflection($function): self
142
    {
143
        $this->name = $function->getName();
48✔
144

145
        // Extract PHP 8.0+ attributes
146
        $this->attributes = Utils::extractAttributesFromReflection($function);
48✔
147

148
        if (!$this->line) {
48✔
149
            $lineTmp = $function->getStartLine();
×
150
            if ($lineTmp !== false) {
×
151
                $this->line = $lineTmp;
×
152
            }
153
        }
154

155
        $file = $function->getFileName();
48✔
156
        if ($file) {
48✔
157
            $this->file = $file;
40✔
158
        }
159

160
        $returnType = $function->getReturnType();
48✔
161
        if ($returnType !== null) {
48✔
162
            if (\method_exists($returnType, 'getName')) {
×
163
                $this->returnType = $returnType->getName();
×
164
            } else {
165
                $this->returnType = $returnType . '';
×
166
            }
167

168
            if ($returnType->allowsNull()) {
×
169
                if ($this->returnType && $this->returnType !== 'null' && \strpos($this->returnType, 'null|') !== 0) {
×
170
                    $this->returnType = 'null|' . $this->returnType;
×
171
                } elseif (!$this->returnType) {
×
172
                    $this->returnType = 'null|mixed';
×
173
                }
174
            }
175
        }
176

177
        $docComment = $function->getDocComment();
48✔
178
        if ($docComment) {
48✔
179
            $this->readPhpDoc($docComment);
40✔
180
        }
181

182
        if (!$this->returnTypeFromPhpDoc) {
48✔
183
            try {
184
                $phpDoc = DocFactoryProvider::getDocFactory()->create((string)$function->getDocComment());
24✔
185
                $returnTypeTmp = $phpDoc->getTagsByName('return');
×
186
                if (
187
                    \count($returnTypeTmp) === 1
×
188
                    &&
189
                    $returnTypeTmp[0] instanceof \phpDocumentor\Reflection\DocBlock\Tags\Return_
×
190
                ) {
191
                    $this->returnTypeFromPhpDoc = Utils::parseDocTypeObject($returnTypeTmp[0]->getType());
×
192
                }
193
            } catch (\Exception $e) {
24✔
194
                // ignore
195
            }
196
        }
197

198
        foreach ($function->getParameters() as $parameter) {
48✔
199
            $param = (new PHPParameter($this->parserContainer))->readObjectFromReflection($parameter);
48✔
200
            $this->parameters[$param->name] = $param;
48✔
201
        }
202

203
        $docComment = $function->getDocComment();
48✔
204
        if ($docComment) {
48✔
205
            $this->readPhpDoc($docComment);
40✔
206
        }
207

208
        return $this;
48✔
209
    }
210

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

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

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

228
        return null;
×
229
    }
230

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

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

248
            $parsedReturnTag = $phpDoc->getTagsByName('return');
198✔
249

250
            if (!empty($parsedReturnTag)) {
198✔
251
                /** @var Return_ $parsedReturnTagReturn */
252
                $parsedReturnTagReturn = $parsedReturnTag[0];
158✔
253

254
                if ($parsedReturnTagReturn instanceof Return_) {
158✔
255
                    $this->returnTypeFromPhpDocMaybeWithComment = \trim((string) $parsedReturnTagReturn);
158✔
256

257
                    $type = $parsedReturnTagReturn->getType();
158✔
258

259
                    $this->returnTypeFromPhpDoc = Utils::normalizePhpType(\ltrim((string) $type, '\\'));
158✔
260

261
                    $typeTmp = Utils::parseDocTypeObject($type);
158✔
262
                    if ($typeTmp !== '') {
158✔
263
                        $this->returnTypeFromPhpDocSimple = $typeTmp;
158✔
264
                    }
265
                }
266

267
                $this->returnPhpDocRaw = (string) $parsedReturnTagReturn;
158✔
268
                $this->returnTypeFromPhpDocExtended = Utils::modernPhpdoc((string) $parsedReturnTagReturn);
158✔
269
            }
270

271
            $parsedReturnTag = $phpDoc->getTagsByName('psalm-return')
198✔
272
                               + $phpDoc->getTagsByName('phpstan-return');
198✔
273

274
            if (!empty($parsedReturnTag) && $parsedReturnTag[0] instanceof Generic) {
198✔
275
                $parsedReturnTagReturn = (string) $parsedReturnTag[0];
134✔
276

277
                $this->returnTypeFromPhpDocExtended = Utils::modernPhpdoc($parsedReturnTagReturn);
198✔
278
            }
279
        } catch (\Exception $e) {
8✔
280
            $tmpErrorMessage = \sprintf(
8✔
281
                '%s:%s | %s',
8✔
282
                $this->name,
8✔
283
                $this->line ?? '?',
8✔
284
                \print_r($e->getMessage(), true)
8✔
285
            );
8✔
286
            $this->parseError[\md5($tmpErrorMessage)] = $tmpErrorMessage;
8✔
287
        }
288

289
        try {
290
            $this->readPhpDocByTokens($docComment);
198✔
UNCOV
291
        } catch (\Exception $e) {
×
UNCOV
292
            $tmpErrorMessage = $this->name . ':' . ($this->line ?? '?') . ' | ' . \print_r($e->getMessage(), true);
×
UNCOV
293
            $this->parseError[\md5($tmpErrorMessage)] = $tmpErrorMessage;
×
294
        }
295
    }
296

297
    /**
298
     * @throws \PHPStan\PhpDocParser\Parser\ParserException
299
     */
300
    private function readPhpDocByTokens(string $docComment): void
301
    {
302
        $tokens = Utils::modernPhpdocTokens($docComment);
198✔
303

304
        // Track standard (@return) and extended (@phpstan-return / @psalm-return) content separately
305
        // so that the more specific phpstan/psalm annotation always wins regardless of tag order.
306
        $returnContent = null;
198✔
307
        $extendedReturnContent = null;
198✔
308
        $currentTarget = null; // 'standard' | 'extended'
198✔
309

310
        foreach ($tokens->getTokens() as $token) {
198✔
311
            $content = $token[0];
198✔
312

313
            if ($content === '@return') {
198✔
314
                $currentTarget = 'standard';
158✔
315
                $returnContent = '';
158✔
316
                continue;
158✔
317
            }
318

319
            if ($content === '@psalm-return' || $content === '@phpstan-return') {
198✔
320
                $currentTarget = 'extended';
134✔
321
                $extendedReturnContent = '';
134✔
322
                continue;
134✔
323
            }
324

325
            // We can stop if we found the end.
326
            if ($content === '*/') {
198✔
327
                break;
198✔
328
            }
329

330
            if ($currentTarget === 'standard') {
198✔
331
                $returnContent .= $content;
158✔
332
            } elseif ($currentTarget === 'extended') {
198✔
333
                $extendedReturnContent .= $content;
134✔
334
            }
335
        }
336

337
        // Prefer @phpstan-return / @psalm-return over plain @return regardless of tag order.
338
        $bestContent = null;
198✔
339
        if ($extendedReturnContent !== null && \trim($extendedReturnContent) !== '') {
198✔
340
            $bestContent = \trim($extendedReturnContent);
126✔
341
        } elseif ($returnContent !== null && \trim($returnContent) !== '') {
182✔
342
            $bestContent = \trim($returnContent);
134✔
343
        }
344

345
        if ($bestContent) {
198✔
346
            if (!$this->returnPhpDocRaw) {
158✔
347
                $this->returnPhpDocRaw = $bestContent;
32✔
348
            }
349
            try {
350
                $this->returnTypeFromPhpDocExtended = Utils::modernPhpdoc($bestContent);
158✔
351
            } catch (\PHPStan\PhpDocParser\Parser\ParserException $e) {
24✔
352
                $recoveredType = Utils::recoverBrokenPhpdocType($bestContent);
24✔
353
                if ($recoveredType !== null) {
24✔
354
                    $normalizedRecoveredType = Utils::normalizePhpType($recoveredType);
24✔
355
                    $this->returnTypeFromPhpDoc = $this->returnTypeFromPhpDoc ?? $normalizedRecoveredType;
24✔
356
                    $this->returnTypeFromPhpDocSimple = $this->returnTypeFromPhpDocSimple ?? $normalizedRecoveredType;
24✔
357
                    $this->returnTypeFromPhpDocExtended = $recoveredType;
24✔
358
                }
359

360
                $tmpErrorMessage = $this->name . ':' . ($this->line ?? '?') . ' | ' . $e->getMessage();
24✔
361
                $this->parseError[\md5($tmpErrorMessage)] = $tmpErrorMessage;
24✔
362
            }
363
        }
364
    }
365
}
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