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

voku / Simple-PHP-Code-Parser / 24286329431

11 Apr 2026 04:09PM UTC coverage: 82.898% (+0.01%) from 82.886%
24286329431

Pull #83

github

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

178 of 221 new or added lines in 7 files covered. (80.54%)

33 existing lines in 4 files now uncovered.

1682 of 2029 relevant lines covered (82.9%)

36.9 hits per line

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

70.86
/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);
33✔
56

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

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

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

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

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

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

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

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

133
        return $this;
33✔
134
    }
135

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

289
        try {
290
            $this->readPhpDocByTokens($docComment);
55✔
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);
55✔
303

304
        $returnContent = null;
55✔
305
        foreach ($tokens->getTokens() as $token) {
55✔
306
            $content = $token[0];
55✔
307

308
            if ($content === '@return' || $content === '@psalm-return' || $content === '@phpstan-return') {
55✔
309
                // reset
310
                $returnContent = '';
52✔
311

312
                continue;
52✔
313
            }
314

315
            // We can stop if we found the end.
316
            if ($content === '*/') {
55✔
317
                break;
55✔
318
            }
319

320
            if ($returnContent !== null) {
55✔
321
                $returnContent .= $content;
52✔
322
            }
323
        }
324

325
        $returnContent = $returnContent ? \trim($returnContent) : null;
55✔
326
        if ($returnContent) {
55✔
327
            if (!$this->returnPhpDocRaw) {
49✔
328
                $this->returnPhpDocRaw = $returnContent;
12✔
329
            }
330
            try {
331
                $this->returnTypeFromPhpDocExtended = Utils::modernPhpdoc($returnContent);
49✔
332
            } catch (\PHPStan\PhpDocParser\Parser\ParserException $e) {
9✔
333
                $recoveredType = Utils::recoverBrokenPhpdocType($returnContent);
9✔
334
                if ($recoveredType !== null) {
9✔
335
                    $normalizedRecoveredType = Utils::normalizePhpType($recoveredType);
9✔
336
                    $this->returnTypeFromPhpDoc = $this->returnTypeFromPhpDoc ?? $normalizedRecoveredType;
9✔
337
                    $this->returnTypeFromPhpDocSimple = $this->returnTypeFromPhpDocSimple ?? $normalizedRecoveredType;
9✔
338
                    $this->returnTypeFromPhpDocExtended = $recoveredType;
9✔
339
                }
340

341
                $tmpErrorMessage = $this->name . ':' . ($this->line ?? '?') . ' | ' . $e->getMessage();
9✔
342
                $this->parseError[\md5($tmpErrorMessage)] = $tmpErrorMessage;
9✔
343
            }
344
        }
345
    }
346
}
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