Passed
Push — master ( f79d5d...3fd334 )
by
unknown
19:27
created

GeneratorClassesResolver   A

Complexity

Total Complexity 18

Size/Duplication

Total Lines 101
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 18
eloc 37
dl 0
loc 101
rs 10
c 0
b 0
f 0

5 Methods

Rating   Name   Duplication   Size   Complexity  
A duplicateNodeAttributes() 0 6 2
A __construct() 0 3 1
A substituteClassString() 0 12 3
B enterNode() 0 22 9
A substituteMakeInstance() 0 14 3
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the TYPO3 CMS project.
7
 *
8
 * It is free software; you can redistribute it and/or modify it under
9
 * the terms of the GNU General Public License, either version 2
10
 * of the License, or any later version.
11
 *
12
 * For the full copyright and license information, please read the
13
 * LICENSE.txt file that was distributed with this source code.
14
 *
15
 * The TYPO3 project - inspiring people to share!
16
 */
17
18
namespace TYPO3\CMS\Install\ExtensionScanner\Php;
19
20
use PhpParser\BuilderFactory;
21
use PhpParser\Node;
22
use PhpParser\Node\Expr;
23
use PhpParser\Node\Expr\ClassConstFetch;
24
use PhpParser\Node\Expr\New_;
25
use PhpParser\Node\Expr\StaticCall;
26
use PhpParser\Node\Name\FullyQualified;
27
use PhpParser\Node\Scalar\String_;
28
use PhpParser\NodeVisitorAbstract;
29
use TYPO3\CMS\Core\Utility\GeneralUtility;
30
use TYPO3\CMS\Install\ExtensionScanner\Php\Matcher\AbstractCoreMatcher;
31
32
/**
33
 * Create a fully qualified class name object from first argument of
34
 * GeneralUtility::makeInstance('My\\Package\\Class\\Name') if given as string
35
 * and not as My\Package\Class\Name::class language construct.
36
 *
37
 * This resolver is to be called after generic NameResolver::class, but before
38
 * other search and find visitors that implement CodeScannerInterface::class
39
 * @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
40
 */
41
class GeneratorClassesResolver extends NodeVisitorAbstract
42
{
43
    /**
44
     * @var BuilderFactory
45
     */
46
    protected $builderFactory;
47
48
    public function __construct(BuilderFactory $builderFactory = null)
49
    {
50
        $this->builderFactory = $builderFactory ?? new BuilderFactory();
51
    }
52
53
    /**
54
     * Called by PhpParser.
55
     * Create an fqdn object from first makeInstance argument if it is a String
56
     *
57
     * @param Node $node Incoming node
58
     */
59
    public function enterNode(Node $node)
60
    {
61
        if ($node instanceof StaticCall
62
            && $node->class instanceof FullyQualified
63
            && $node->class->toString() === GeneralUtility::class
64
            && $node->name->name === 'makeInstance'
65
            && isset($node->args[0]->value)
66
            && $node->args[0]->value instanceof Expr
67
        ) {
68
            $argValue = $node->args[0]->value;
69
            $argAlternative = $this->substituteClassString($argValue);
70
            if ($argAlternative !== null) {
71
                $node->args[0]->value = $argAlternative;
72
                $argValue = $argAlternative;
73
            }
74
75
            $nodeAlternative = $this->substituteMakeInstance($node, $argValue);
76
            if ($nodeAlternative !== null) {
77
                $node->setAttribute(AbstractCoreMatcher::NODE_RESOLVED_AS, $nodeAlternative);
78
            }
79
        }
80
        return null;
81
    }
82
83
    /**
84
     * Substitutes class-string values with their corresponding class constant
85
     * representation (`'Vendor\\ClassName'` -> `\Vendor\ClassName::class`).
86
     *
87
     * @param Expr $argValue
88
     * @return ClassConstFetch|null
89
     */
90
    protected function substituteClassString(Expr $argValue): ?ClassConstFetch
91
    {
92
        // skip non-strings, and those starting with (invalid) namespace separator
93
        if (!$argValue instanceof String_ || $argValue->value[0] === '\\') {
94
            return null;
95
        }
96
97
        $classString = ltrim($argValue->value, '\\');
98
        $className = new FullyQualified($classString);
99
        $classArg = $this->builderFactory->classConstFetch($className, 'class');
100
        $this->duplicateNodeAttributes($argValue, $className, $classArg);
101
        return $classArg;
102
    }
103
104
    /**
105
     * Substitutes `makeInstance` invocations with proper `new` invocations.
106
     * `GeneralUtility(\Vendor\ClassName::class, 'a', 'b')` -> `new \Vendor\ClassName('a', 'b')`
107
     *
108
     * @param StaticCall $node
109
     * @param Expr $argValue
110
     * @return New_|null
111
     */
112
    protected function substituteMakeInstance(StaticCall $node, Expr $argValue): ?New_
113
    {
114
        if (!$argValue instanceof ClassConstFetch
115
            || !$argValue->class instanceof FullyQualified
116
        ) {
117
            return null;
118
        }
119
120
        $newExpr = $this->builderFactory->new(
121
            $argValue->class,
122
            array_slice($node->args, 1),
123
        );
124
        $this->duplicateNodeAttributes($node, $newExpr);
125
        return $newExpr;
126
    }
127
128
    /**
129
     * Duplicates node positions in source file, based on the assumption
130
     * that only lines are relevant. In case this shall be used for
131
     * code-migration, real offset positions would be required.
132
     *
133
     * @param Node $source
134
     * @param Node ...$targets
135
     */
136
    protected function duplicateNodeAttributes(Node $source, Node ...$targets): void
137
    {
138
        foreach ($targets as $target) {
139
            $target->setAttributes([
140
                'startLine' => $source->getStartLine(),
141
                'endLine' => $source->getEndLine(),
142
            ]);
143
        }
144
    }
145
}
146