Passed
Push — master ( 943241...45600c )
by Paul
10:51 queued 05:35
created

CodeParser::findClassAndUses()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 7
nc 3
nop 1
dl 0
loc 12
rs 9.2
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * This file is part of PHPUnit Generator.
5
 *
6
 * (c) Paul Thébaud <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace PHPUnitGenerator\Parser;
13
14
use PhpParser\Error;
15
use PhpParser\Node;
16
use PhpParser\Node\Name;
17
use PhpParser\Node\NullableType;
18
use PhpParser\Node\Param;
19
use PhpParser\Node\Stmt\Class_;
20
use PhpParser\Node\Stmt\ClassMethod;
21
use PhpParser\Node\Stmt\Interface_;
22
use PhpParser\Node\Stmt\Namespace_;
23
use PhpParser\Node\Stmt\Property;
24
use PhpParser\Node\Stmt\Trait_;
25
use PhpParser\Node\Stmt\Use_;
26
use PhpParser\Parser;
27
use PhpParser\ParserFactory;
28
use PHPUnitGenerator\Config\ConfigInterface\ConfigInterface;
29
use PHPUnitGenerator\Exception\EmptyFileException;
30
use PHPUnitGenerator\Exception\InvalidCodeException;
31
use PHPUnitGenerator\Exception\NoMethodFoundException;
32
use PHPUnitGenerator\Model\ArgumentModel;
33
use PHPUnitGenerator\Model\ClassModel;
34
use PHPUnitGenerator\Model\MethodModel;
35
use PHPUnitGenerator\Model\ModelInterface\ArgumentModelInterface;
36
use PHPUnitGenerator\Model\ModelInterface\ClassModelInterface;
37
use PHPUnitGenerator\Model\ModelInterface\MethodModelInterface;
38
use PHPUnitGenerator\Model\ModelInterface\ModifierInterface;
39
use PHPUnitGenerator\Model\ModelInterface\TypeInterface;
40
use PHPUnitGenerator\Parser\ParserInterface\CodeParserInterface;
41
42
/**
43
 * Class CodeParser
44
 *
45
 *      An implementation of CodeParserInterface using Nikic PHP Parser
46
 *
47
 * @see     https://github.com/nikic/PHP-Parser
48
 *
49
 * @package PHPUnitGenerator\Parser
50
 */
51
class CodeParser implements CodeParserInterface
52
{
53
    /**
54
     * @var ConfigInterface $config
55
     */
56
    protected $config;
57
58
    /**
59
     * @var Parser $phpParser The PHP code parser
60
     */
61
    private $phpParser;
62
63
    /**
64
     * @var array $mappingUse An array which map class names and complete class
65
     *      names
66
     */
67
    private $mappingClassNames = [];
68
69
    /**
70
     * CodeParser constructor.
71
     */
72
    public function __construct(ConfigInterface $config)
73
    {
74
        $this->config = $config;
75
76
        $this->phpParser = (new ParserFactory())
77
            ->create(ParserFactory::PREFER_PHP7);
78
    }
79
80
    /**
81
     * {@inheritdoc}
82
     */
83
    public function parse(string $code): ClassModelInterface
84
    {
85
        // Parse the code
86
        try {
87
            $statements = $this->phpParser->parse($code);
88
        } catch (Error $error) {
89
            throw new InvalidCodeException(InvalidCodeException::TEXT);
90
        }
91
92
        // Search if there is a namespace
93
        $namespaceName = null;
94
        $namespaceStatement = $this->findNamespace($statements);
95
        // If namespace is defined, search in namespace statements
96
        if ($namespaceStatement !== null) {
97
            $statements = $namespaceStatement->stmts ?? [];
98
            $namespaceName = $namespaceStatement->name ? $namespaceStatement->name->toString() : null;
99
        }
100
101
        // Parse class
102
        $classStatement = $this->findClassAndUses($statements);
103
        if (! $classStatement) {
104
            throw new EmptyFileException(EmptyFileException::TEXT);
105
        }
106
107
        // Create class model
108
        $classModel = new ClassModel($classStatement->name);
0 ignored issues
show
Bug introduced by
It seems like $classStatement->name can also be of type null; however, parameter $name of PHPUnitGenerator\Model\ClassModel::__construct() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

108
        $classModel = new ClassModel(/** @scrutinizer ignore-type */ $classStatement->name);
Loading history...
109
        if ($namespaceName !== null) {
110
            $classModel->setNamespaceName($namespaceName);
111
        }
112
        $classModel = $this->parseTypeAndModifier($classModel, $classStatement);
113
114
        // Add "self" as an alias of class name to mappingClassNames
115
        $this->mappingClassNames['self'] = $classModel->getCompleteName();
116
117
        // Add future tests documentation
118
        $classModel->setTestsAnnotations($this->getTestsAnnotations());
119
120
        // Parse class methods
121
        $classModel->setMethods($this->parseMethods($classModel, $classStatement->stmts));
122
123
        if (count($classModel->getMethods()) === 0) {
124
            throw new NoMethodFoundException(
125
                sprintf(NoMethodFoundException::TEXT, $classModel->getName())
126
            );
127
        }
128
129
        // Parse class properties
130
        $classModel->setProperties(
131
            $this->parseProperties($classStatement->stmts)
132
        );
133
134
        return $classModel;
135
    }
136
137
    /**
138
     * Get the namespace statement if exists, else return null
139
     *
140
     * @param Node[] $statements
141
     *
142
     * @return Namespace_|null
143
     */
144
    protected function findNamespace(array $statements)
145
    {
146
        foreach ($statements as $statement) {
147
            if ($statement instanceof Namespace_) {
148
                return $statement;
149
            }
150
        }
151
        return null;
152
    }
153
154
    /**
155
     * Find class statement if exists, and parse uses statement
156
     *
157
     * @param array $statements
158
     *
159
     * @return Class_|Trait_|Interface_|null
160
     */
161
    protected function findClassAndUses(array $statements)
162
    {
163
        $classStatement = null;
164
        foreach ($statements as $statement) {
165
            if ($statement instanceof Use_ && $statement->type === Use_::TYPE_NORMAL) {
166
                $this->parseUseStatement($statement);
167
            } else {
168
                // No break, because uses statement can be after a class statement
169
                $classStatement = $this->findClass($statement);
170
            }
171
        }
172
        return $classStatement;
173
    }
174
175
    /**
176
     * Find class in statement if exists
177
     *
178
     * @param Node $statement
179
     *
180
     * @return Class_|Trait_|Interface_|null
181
     */
182
    protected function findClass(Node $statement)
183
    {
184
        if ($statement instanceof Class_ || $statement instanceof Trait_ || $statement instanceof Interface_) {
185
            return $statement;
186
        }
187
        return null;
188
    }
189
190
    /**
191
     * Parse a "use" statement and save mapping between alias and class name
192
     *
193
     * @param Use_ $statement
194
     */
195
    protected function parseUseStatement(Use_ $statement)
196
    {
197
        foreach ($statement->uses as $use) {
198
            if ($use->alias) {
199
                $this->mappingClassNames[$use->alias] = $use->name->toString();
200
            } else {
201
                $this->mappingClassNames[$use->name->getLast()] = $use->name->toString();
202
            }
203
        }
204
    }
205
206
    protected function parseTypeAndModifier(ClassModelInterface $classModel, Node $statement): ClassModelInterface
207
    {
208
        if ($statement instanceof Class_) {
209
            if ($statement->isFinal()) {
210
                $classModel->setModifier(ModifierInterface::MODIFIER_FINAL);
211
            } elseif ($statement->isAbstract()) {
212
                $classModel->setModifier(ModifierInterface::MODIFIER_ABSTRACT);
213
            }
214
        } elseif ($statement instanceof Interface_) {
215
            $classModel->setType(ClassModelInterface::TYPE_INTERFACE);
216
        } elseif ($statement instanceof Trait_) {
217
            $classModel->setType(ClassModelInterface::TYPE_TRAIT);
218
        }
219
        return $classModel;
220
    }
221
222
    /**
223
     * Parse class statements to find properties
224
     *
225
     * @param Node[] $statements
226
     *
227
     * @return string[]
228
     */
229
    protected function parseProperties(array $statements): array
230
    {
231
        $properties = [];
232
        foreach ($statements as $statement) {
233
            if ($statement instanceof Property) {
234
                $properties[] = $this->parseProperty($statement);
235
            }
236
        }
237
        return $properties;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $properties returns an array which contains values of type array which are incompatible with the documented value type string.
Loading history...
238
    }
239
240
    /**
241
     * Parse a "property" statement to get all declared properties
242
     *
243
     * @param Property $statement
244
     *
245
     * @return array
246
     */
247
    protected function parseProperty(Property $statement): array
248
    {
249
        $properties = [];
250
        foreach ($statement->props as $property) {
251
            $properties[] = $property->name;
252
        }
253
        return $properties;
254
    }
255
256
    /**
257
     * Get the tests annotations from the configuration
258
     *
259
     * @return array
260
     */
261
    protected function getTestsAnnotations(): array
262
    {
263
        $testsAnnotations = [];
264
        if (! empty($author = $this->config->getOption(ConfigInterface::OPTION_DOC_AUTHOR))) {
265
            $testsAnnotations['author'] = $author;
266
        }
267
        if (! empty($copyright = $this->config->getOption(ConfigInterface::OPTION_DOC_COPYRIGHT))) {
268
            $testsAnnotations['copyright'] = $copyright;
269
        }
270
        if (! empty($licence = $this->config->getOption(ConfigInterface::OPTION_DOC_LICENCE))) {
271
            $testsAnnotations['licence'] = $licence;
272
        }
273
        if (! empty($since = $this->config->getOption(ConfigInterface::OPTION_DOC_SINCE))) {
274
            $testsAnnotations['since'] = $since;
275
        }
276
        return $testsAnnotations;
277
    }
278
279
    /**
280
     * Parse class statements to find methods
281
     *
282
     * @param ClassModelInterface $classModel
283
     * @param Node[]              $statements
284
     *
285
     * @return MethodModelInterface[]
286
     */
287
    protected function parseMethods(
288
        ClassModelInterface $classModel,
289
        array $statements
290
    ): array {
291
        $methods = [];
292
        foreach ($statements as $statement) {
293
            if ($statement instanceof ClassMethod) {
294
                $methodModel = new MethodModel($classModel, $statement->name);
295
296
                // Get method visibility
297
                if ($statement->isProtected()) {
298
                    $methodModel->setVisibility(MethodModelInterface::VISIBILITY_PROTECTED);
299
                } elseif ($statement->isPrivate()) {
300
                    $methodModel->setVisibility(MethodModelInterface::VISIBILITY_PRIVATE);
301
                }
302
303
                // Get method modifier
304
                $modifiers = [];
305
                if ($statement->isStatic()) {
306
                    $modifiers[] = ModifierInterface::MODIFIER_STATIC;
307
                }
308
                if ($statement->isFinal()) {
309
                    $modifiers[] = ModifierInterface::MODIFIER_FINAL;
310
                } elseif ($statement->isAbstract()) {
311
                    $modifiers[] = ModifierInterface::MODIFIER_ABSTRACT;
312
                }
313
                $methodModel->setModifiers($modifiers);
314
315
                // Get method arguments
316
                $methodModel->setArguments(
317
                    $this->parseArguments(
318
                        $methodModel,
319
                        $statement->getParams()
320
                    )
321
                );
322
323
                // Get method return type
324
                $returnType = $statement->getReturnType();
325
                if ($returnType instanceof NullableType) {
326
                    $returnType = $returnType->type;
327
                    $methodModel->setReturnNullable(true);
328
                }
329
                $methodModel->setReturnType($this->parseType($methodModel->getParentClass(), $returnType));
330
331
                // Get method documentation
332
                if ($statement->getDocComment()) {
333
                    $methodModel->setDocumentation($statement->getDocComment()->getText());
334
                }
335
336
                $methods[] = $methodModel;
337
            }
338
        }
339
        return $methods;
340
    }
341
342
    /**
343
     * Parse method arguments to create them
344
     *
345
     * @param MethodModelInterface $methodModel
346
     * @param Param[]              $statements
347
     *
348
     * @return ArgumentModelInterface[]
349
     */
350
    protected function parseArguments(
351
        MethodModelInterface $methodModel,
352
        array $statements
353
    ): array {
354
        $arguments = [];
355
        foreach ($statements as $statement) {
356
            if ($statement instanceof Param) {
357
                $argumentModel = new ArgumentModel(
358
                    $methodModel,
359
                    $statement->name
360
                );
361
362
                // Get argument type
363
                $type = $statement->type;
364
                if ($type instanceof NullableType) {
365
                    $type = $type->type;
366
                    $argumentModel->setNullable(true);
367
                }
368
                $argumentModel->setType($this->parseType($methodModel->getParentClass(), $type));
369
370
                // @todo: Add default value
371
                // $argumentModel->setDefaultValue();
372
373
                $arguments[] = $argumentModel;
374
            }
375
        }
376
        return $arguments;
377
    }
378
379
    /**
380
     * Parse a type to get a valid TypeInterface type
381
     *
382
     * @param ClassModelInterface $classModel
383
     * @param Name|string         $type
384
     *
385
     * @return string
386
     */
387
    protected function parseType(ClassModelInterface $classModel, $type): string
388
    {
389
        // Its empty
390
        if ($type === null) {
391
            return TypeInterface::TYPE_MIXED;
392
        }
393
        // Its an object
394
        if ($type instanceof Name) {
395
            $type = $type->__toString();
396
            if (substr($type, 0, 1) === '\\') {
397
                return substr($type, 1);
398
            }
399
            if (isset($this->mappingClassNames[$type])) {
400
                return $this->mappingClassNames[$type];
401
            }
402
            if (class_exists('\\' . $type)) {
403
                return $type;
404
            }
405
            return ($classModel->getNamespaceName() ? ($classModel->getNamespaceName() . '\\') : '') . $type;
406
        }
407
        return constant(TypeInterface::class . '::TYPE_' . strtoupper($type));
408
    }
409
}
410