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

voku / Simple-PHP-Code-Parser / 29085974904

10 Jul 2026 10:10AM UTC coverage: 85.103% (+0.3%) from 84.815%
29085974904

push

github

moellekenl
[+]: Add support for source-range information and enhance PHP model elements

- Introduced `endLine`, `startFilePos`, and `endFilePos` properties to model elements for precise source navigation.
- Added `PHPFileInfo` class for lightweight file summaries and improved AST access methods.
- Enhanced trait handling in class-like models to include trait uses and adaptations.
- Updated method and parameter models to include `is_returned_by_ref` and `is_promoted` properties.
- Improved PHPDoc context handling for better type resolution in models.

269 of 316 new or added lines in 16 files covered. (85.13%)

1 existing line in 1 file now uncovered.

1988 of 2336 relevant lines covered (85.1%)

175.85 hits per line

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

72.22
/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 phpDocumentor\Reflection\DocBlock\Tags\Throws;
10
use PhpParser\Comment\Doc;
11
use PhpParser\Node\Stmt\Function_;
12
use ReflectionFunction;
13
use voku\SimplePhpParser\Parsers\Helper\DocFactoryProvider;
14
use voku\SimplePhpParser\Parsers\Helper\Utils;
15

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

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

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

32
    /**
33
     * Whether this function or method returns by reference.
34
     */
35
    public ?bool $is_returned_by_ref = null;
36

37
    /**
38
     * Exception types documented with @throws, in source order.
39
     *
40
     * @var string[]
41
     */
42
    public array $throws = [];
43

44
    public ?string $returnPhpDocRaw = null;
45

46
    public ?string $returnType = null;
47

48
    public ?string $returnTypeFromPhpDoc = null;
49

50
    /**
51
     * Import-aware equivalent of $returnTypeFromPhpDoc for AST-backed models.
52
     */
53
    public ?string $returnTypeFromPhpDocResolved = null;
54

55
    public ?string $returnTypeFromPhpDocSimple = null;
56

57
    public ?string $returnTypeFromPhpDocExtended = null;
58

59
    public ?string $returnTypeFromPhpDocMaybeWithComment = null;
60

61
    public string $summary = '';
62

63
    public string $description = '';
64

65
    /**
66
     * @param Function_   $node
67
     * @param string|null $dummy
68
     *
69
     * @return $this
70
     */
71
    public function readObjectFromPhpNode($node, $dummy = null): self
72
    {
73
        $this->prepareNode($node);
150✔
74

75
        $this->name = static::getFQN($node);
150✔
76

77
        $this->is_returned_by_ref = $node->byRef;
150✔
78

79
        // Extract PHP 8.0+ attributes
80
        if (!empty($node->attrGroups)) {
150✔
81
            $this->attributes = Utils::extractAttributesFromAstNode($node->attrGroups);
10✔
82
        }
83

84
        /** @noinspection NotOptimalIfConditionsInspection */
85
        if (\function_exists($this->name)) {
150✔
86
            $reflectionFunction = Utils::createFunctionReflectionInstance($this->name);
70✔
87
            $this->readObjectFromReflection($reflectionFunction);
70✔
88
        }
89

90
        if ($node->returnType) {
150✔
91
            if (!$this->returnType) {
40✔
92
                $returnTypeStr = Utils::typeNodeToString($node->returnType);
40✔
93
                if ($returnTypeStr !== null) {
40✔
94
                    $this->returnType = $returnTypeStr;
40✔
95
                }
96
            }
97

98
            if ($node->returnType instanceof \PhpParser\Node\NullableType) {
40✔
99
                if ($this->returnType && $this->returnType !== 'null' && \strpos($this->returnType, 'null|') !== 0) {
×
100
                    $this->returnType = 'null|' . $this->returnType;
×
101
                } elseif (!$this->returnType) {
×
102
                    $this->returnType = 'null|mixed';
×
103
                }
104
            }
105
        }
106

107
        $docComment = $node->getDocComment();
150✔
108
        if ($docComment) {
150✔
109
            try {
110
                $phpDoc = DocFactoryProvider::getDocFactory()->create($docComment->getText());
90✔
111
                $this->summary = $phpDoc->getSummary();
90✔
112
                $this->description = (string) $phpDoc->getDescription();
90✔
113
            } catch (\Exception $e) {
×
114
                $tmpErrorMessage = \sprintf(
×
115
                    '%s:%s | %s',
×
116
                    $this->name,
×
117
                    $this->line ?? '?',
×
118
                    \print_r($e->getMessage(), true)
×
119
                );
×
120
                $this->parseError[\md5($tmpErrorMessage)] = $tmpErrorMessage;
×
121
            }
122
        }
123

124
        foreach ($node->getParams() as $parameter) {
150✔
125
            $parameterVar = $parameter->var;
130✔
126
            if ($parameterVar instanceof \PhpParser\Node\Expr\Error) {
130✔
127
                $this->parseError[] = \sprintf(
×
128
                    '%s:%s | maybe at this position an expression is required',
×
129
                    $this->line ?? '?',
×
130
                    $this->pos ?? ''
×
131
                );
×
132

133
                return $this;
×
134
            }
135

136
            $paramNameTmp = $parameterVar->name;
130✔
137
            \assert(\is_string($paramNameTmp));
138

139
            if (isset($this->parameters[$paramNameTmp])) {
130✔
140
                $this->parameters[$paramNameTmp] = $this->parameters[$paramNameTmp]->readObjectFromPhpNode($parameter, $node);
70✔
141
            } else {
142
                $this->parameters[$paramNameTmp] = (new PHPParameter($this->parserContainer))->readObjectFromPhpNode($parameter, $node);
70✔
143
            }
144
        }
145

146
        $this->collectTags($node);
150✔
147

148
        $docComment = $node->getDocComment();
150✔
149
        if ($docComment) {
150✔
150
            $this->readPhpDoc($docComment, self::getPhpDocContext($node));
90✔
151
        }
152

153
        return $this;
150✔
154
    }
155

156
    /**
157
     * @param ReflectionFunction $function
158
     *
159
     * @return $this
160
     */
161
    public function readObjectFromReflection($function): self
162
    {
163
        $this->name = $function->getName();
70✔
164

165
        $this->is_returned_by_ref = $function->returnsReference();
70✔
166

167
        // Extract PHP 8.0+ attributes
168
        $this->attributes = Utils::extractAttributesFromReflection($function);
70✔
169

170
        if (!$this->line) {
70✔
171
            $lineTmp = $function->getStartLine();
×
172
            if ($lineTmp !== false) {
×
173
                $this->line = $lineTmp;
×
174
            }
175
        }
176

177
        if ($this->endLine === null) {
70✔
NEW
178
            $endLineTmp = $function->getEndLine();
×
NEW
179
            if ($endLineTmp !== false) {
×
NEW
180
                $this->endLine = $endLineTmp;
×
181
            }
182
        }
183

184
        $file = $function->getFileName();
70✔
185
        if ($file) {
70✔
186
            $this->file = $file;
60✔
187
        }
188

189
        $returnType = $function->getReturnType();
70✔
190
        if ($returnType !== null) {
70✔
191
            if (\method_exists($returnType, 'getName')) {
×
192
                $this->returnType = $returnType->getName();
×
193
            } else {
194
                $this->returnType = $returnType . '';
×
195
            }
196

197
            if ($returnType->allowsNull()) {
×
198
                if ($this->returnType && $this->returnType !== 'null' && \strpos($this->returnType, 'null|') !== 0) {
×
199
                    $this->returnType = 'null|' . $this->returnType;
×
200
                } elseif (!$this->returnType) {
×
201
                    $this->returnType = 'null|mixed';
×
202
                }
203
            }
204
        }
205

206
        $docComment = $function->getDocComment();
70✔
207
        if ($docComment) {
70✔
208
            $this->readPhpDoc($docComment);
60✔
209
        }
210

211
        if (!$this->returnTypeFromPhpDoc) {
70✔
212
            try {
213
                $phpDoc = DocFactoryProvider::getDocFactory()->create((string)$function->getDocComment());
40✔
214
                $returnTypeTmp = $phpDoc->getTagsByName('return');
×
215
                if (
216
                    \count($returnTypeTmp) === 1
×
217
                    &&
218
                    $returnTypeTmp[0] instanceof \phpDocumentor\Reflection\DocBlock\Tags\Return_
×
219
                ) {
220
                    $this->returnTypeFromPhpDoc = Utils::parseDocTypeObject($returnTypeTmp[0]->getType());
×
221
                }
222
            } catch (\Exception $e) {
40✔
223
                // ignore
224
            }
225
        }
226

227
        foreach ($function->getParameters() as $parameter) {
70✔
228
            $param = (new PHPParameter($this->parserContainer))->readObjectFromReflection($parameter);
70✔
229
            $this->parameters[$param->name] = $param;
70✔
230
        }
231

232
        $docComment = $function->getDocComment();
70✔
233
        if ($docComment) {
70✔
234
            $this->readPhpDoc($docComment);
60✔
235
        }
236

237
        return $this;
70✔
238
    }
239

240
    /**
241
     * @return string|null
242
     */
243
    public function getReturnType(): ?string
244
    {
245
        if ($this->returnTypeFromPhpDocExtended) {
×
246
            return $this->returnTypeFromPhpDocExtended;
×
247
        }
248

249
        if ($this->returnType) {
×
250
            return $this->returnType;
×
251
        }
252

253
        if ($this->returnTypeFromPhpDocSimple) {
×
254
            return $this->returnTypeFromPhpDocSimple;
×
255
        }
256

257
        return null;
×
258
    }
259

260
    /**
261
     * @param Doc|string $doc
262
     */
263
    protected function readPhpDoc($doc, ?\phpDocumentor\Reflection\Types\Context $context = null): void
264
    {
265
        if ($doc instanceof Doc) {
278✔
266
            $docComment = $doc->getText();
268✔
267
        } else {
268
            $docComment = $doc;
168✔
269
        }
270
        if ($docComment === '') {
278✔
271
            return;
×
272
        }
273

274
        try {
275
            $phpDoc = DocFactoryProvider::getDocFactory()->create($docComment);
278✔
276

277
            $parsedReturnTag = $phpDoc->getTagsByName('return');
278✔
278

279
            if (!empty($parsedReturnTag)) {
278✔
280
                /** @var Return_ $parsedReturnTagReturn */
281
                $parsedReturnTagReturn = $parsedReturnTag[0];
228✔
282

283
                if ($parsedReturnTagReturn instanceof Return_) {
228✔
284
                    $this->returnTypeFromPhpDocMaybeWithComment = \trim((string) $parsedReturnTagReturn);
228✔
285

286
                    $type = $parsedReturnTagReturn->getType();
228✔
287
                    $this->returnTypeFromPhpDoc = Utils::normalizePhpType(\ltrim((string) $type, '\\'));
228✔
288

289
                    $typeTmp = Utils::parseDocTypeObject($type);
228✔
290
                    if ($typeTmp !== '') {
228✔
291
                        $this->returnTypeFromPhpDocSimple = $typeTmp;
228✔
292
                    }
293
                }
294

295
                $this->returnPhpDocRaw = (string) $parsedReturnTagReturn;
228✔
296
                $this->returnTypeFromPhpDocExtended = Utils::modernPhpdoc((string) $parsedReturnTagReturn);
228✔
297
            }
298

299
            $parsedReturnTag = $phpDoc->getTagsByName('psalm-return')
278✔
300
                               + $phpDoc->getTagsByName('phpstan-return');
278✔
301

302
            if (!empty($parsedReturnTag) && $parsedReturnTag[0] instanceof Generic) {
278✔
303
                $parsedReturnTagReturn = (string) $parsedReturnTag[0];
178✔
304

305
                $this->returnTypeFromPhpDocExtended = Utils::modernPhpdoc($parsedReturnTagReturn);
278✔
306
            }
307

308
        } catch (\Exception $e) {
10✔
309
            $tmpErrorMessage = \sprintf(
10✔
310
                '%s:%s | %s',
10✔
311
                $this->name,
10✔
312
                $this->line ?? '?',
10✔
313
                \print_r($e->getMessage(), true)
10✔
314
            );
10✔
315
            $this->parseError[\md5($tmpErrorMessage)] = $tmpErrorMessage;
10✔
316
        }
317

318
        try {
319
            $resolvedPhpDoc = DocFactoryProvider::getDocFactory()->create($docComment, $context);
278✔
320
            $resolvedReturnTags = $resolvedPhpDoc->getTagsByName('return');
278✔
321
            if (isset($resolvedReturnTags[0]) && $resolvedReturnTags[0] instanceof Return_) {
278✔
322
                $this->returnTypeFromPhpDocResolved = (string)$resolvedReturnTags[0]->getType();
228✔
323
            }
324

325
            foreach ($resolvedPhpDoc->getTagsByName('throws') as $throwsTag) {
278✔
326
                if ($throwsTag instanceof Throws) {
40✔
327
                    $this->throws[] = Utils::normalizePhpType(\ltrim((string)$throwsTag->getType(), '\\')) ?? (string)$throwsTag->getType();
40✔
328
                }
329
            }
NEW
330
        } catch (\Exception $e) {
×
NEW
331
            $tmpErrorMessage = \sprintf('%s:%s | %s', $this->name, $this->line ?? '?', \print_r($e->getMessage(), true));
×
NEW
332
            $this->parseError[\md5($tmpErrorMessage)] = $tmpErrorMessage;
×
333
        }
334

335
        try {
336
            $this->readPhpDocByTokens($docComment);
278✔
337
        } catch (\Exception $e) {
×
338
            $tmpErrorMessage = $this->name . ':' . ($this->line ?? '?') . ' | ' . \print_r($e->getMessage(), true);
×
339
            $this->parseError[\md5($tmpErrorMessage)] = $tmpErrorMessage;
×
340
        }
341
    }
342

343
    /**
344
     * @throws \PHPStan\PhpDocParser\Parser\ParserException
345
     */
346
    private function readPhpDocByTokens(string $docComment): void
347
    {
348
        $tokens = Utils::modernPhpdocTokens($docComment);
278✔
349

350
        // Track standard (@return) and extended (@phpstan-return / @psalm-return) content separately
351
        // so that the more specific phpstan/psalm annotation always wins regardless of tag order.
352
        $returnContent = null;
278✔
353
        $extendedReturnContent = null;
278✔
354
        $currentTarget = null; // 'standard' | 'extended'
278✔
355

356
        foreach ($tokens->getTokens() as $token) {
278✔
357
            $content = $token[0];
278✔
358

359
            if ($content === '@return') {
278✔
360
                $currentTarget = 'standard';
228✔
361
                $returnContent = '';
228✔
362
                continue;
228✔
363
            }
364

365
            if ($content === '@psalm-return' || $content === '@phpstan-return') {
278✔
366
                $currentTarget = 'extended';
178✔
367
                $extendedReturnContent = '';
178✔
368
                continue;
178✔
369
            }
370

371
            // We can stop if we found the end.
372
            if ($content === '*/') {
278✔
373
                break;
278✔
374
            }
375

376
            if ($currentTarget === 'standard') {
278✔
377
                $returnContent .= $content;
228✔
378
            } elseif ($currentTarget === 'extended') {
278✔
379
                $extendedReturnContent .= $content;
178✔
380
            }
381
        }
382

383
        // Prefer @phpstan-return / @psalm-return over plain @return regardless of tag order.
384
        $bestContent = null;
278✔
385
        if ($extendedReturnContent !== null && \trim($extendedReturnContent) !== '') {
278✔
386
            $bestContent = \trim($extendedReturnContent);
168✔
387
        } elseif ($returnContent !== null && \trim($returnContent) !== '') {
258✔
388
            $bestContent = \trim($returnContent);
198✔
389
        }
390

391
        if ($bestContent) {
278✔
392
            if (!$this->returnPhpDocRaw) {
228✔
393
                $this->returnPhpDocRaw = $bestContent;
50✔
394
            }
395
            try {
396
                $this->returnTypeFromPhpDocExtended = Utils::modernPhpdoc($bestContent);
228✔
397
            } catch (\PHPStan\PhpDocParser\Parser\ParserException $e) {
30✔
398
                $recoveredType = Utils::recoverBrokenPhpdocType($bestContent);
30✔
399
                if ($recoveredType !== null) {
30✔
400
                    $normalizedRecoveredType = Utils::normalizePhpType($recoveredType);
30✔
401
                    $this->returnTypeFromPhpDoc = $this->returnTypeFromPhpDoc ?? $normalizedRecoveredType;
30✔
402
                    $this->returnTypeFromPhpDocSimple = $this->returnTypeFromPhpDocSimple ?? $normalizedRecoveredType;
30✔
403
                    $this->returnTypeFromPhpDocExtended = $recoveredType;
30✔
404
                }
405

406
                $tmpErrorMessage = $this->name . ':' . ($this->line ?? '?') . ' | ' . $e->getMessage();
30✔
407
                $this->parseError[\md5($tmpErrorMessage)] = $tmpErrorMessage;
30✔
408
            }
409
        }
410
    }
411
}
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