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

voku / Simple-PHP-Code-Parser / 24287978923

11 Apr 2026 05:41PM UTC coverage: 81.31% (-1.6%) from 82.886%
24287978923

Pull #83

github

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

247 of 288 new or added lines in 7 files covered. (85.76%)

30 existing lines in 3 files now uncovered.

1688 of 2076 relevant lines covered (81.31%)

27.68 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);
22✔
56

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

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

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

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

78
            if ($node->returnType instanceof \PhpParser\Node\NullableType) {
4✔
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();
22✔
88
        if ($docComment) {
22✔
89
            try {
90
                $phpDoc = DocFactoryProvider::getDocFactory()->create($docComment->getText());
16✔
91
                $this->summary = $phpDoc->getSummary();
16✔
92
                $this->description = (string) $phpDoc->getDescription();
16✔
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) {
22✔
105
            $parameterVar = $parameter->var;
22✔
106
            if ($parameterVar instanceof \PhpParser\Node\Expr\Error) {
22✔
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;
22✔
117
            \assert(\is_string($paramNameTmp));
118

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

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

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

133
        return $this;
22✔
134
    }
135

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

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

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

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

160
        $returnType = $function->getReturnType();
12✔
161
        if ($returnType !== null) {
12✔
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();
12✔
178
        if ($docComment) {
12✔
179
            $this->readPhpDoc($docComment);
10✔
180
        }
181

182
        if (!$this->returnTypeFromPhpDoc) {
12✔
183
            try {
184
                $phpDoc = DocFactoryProvider::getDocFactory()->create((string)$function->getDocComment());
6✔
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) {
6✔
194
                // ignore
195
            }
196
        }
197

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

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

208
        return $this;
12✔
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) {
46✔
237
            $docComment = $doc->getText();
44✔
238
        } else {
239
            $docComment = $doc;
30✔
240
        }
241
        if ($docComment === '') {
46✔
242
            return;
×
243
        }
244

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

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

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

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

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

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

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

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

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

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

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

289
        try {
290
            $this->readPhpDocByTokens($docComment);
46✔
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);
46✔
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;
46✔
307
        $extendedReturnContent = null;
46✔
308
        $currentTarget = null; // 'standard' | 'extended'
46✔
309

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

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

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

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

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

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

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

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