Completed
Pull Request — master (#82)
by Loren
05:32
created

BuiltinTypeFixer::isTypeDefinedForContext()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 3
cts 3
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 4
nc 2
nop 2
crap 2
1
<?php
2
/**
3
 * Parser Reflection API
4
 *
5
 * @copyright Copyright 2015, Lisachenko Alexander <[email protected]>
6
 *
7
 * This source file is subject to the license that is bundled
8
 * with this source code in the file LICENSE.
9
 */
10
11
namespace Go\ParserReflection\NodeVisitor;
12
13
use PhpParser\Node;
14
use PhpParser\Node\Expr;
15
use PhpParser\Node\Name;
16
use PhpParser\Node\Stmt;
17
use PhpParser\NodeVisitorAbstract;
18
19
class BuiltinTypeFixer extends NodeVisitorAbstract
20
{
21
    const PARAMETER_TYPES = 1;
22
    const RETURN_TYPES    = 2;
23
24
    /** @var array Current list of valid builtin typehints */
25
    protected $supportedBuiltinTypeHints;
26
27
    /**
28
     * Constructs a name resolution visitor.
29
     *
30
     * Options: If "preserveOriginalNames" is enabled, an "originalName" attribute will be added to
31
     * all name nodes that underwent resolution.
32
     *
33
     * @param array $options Options
34
     */
35 1977
    public function __construct(array $options = [])
36
    {
37 1977
        $this->supportedBuiltinTypeHints = [];
38 1977
        if (isset($options['supportedBuiltinTypeHints'])) {
39 4
            if (!is_array($options['supportedBuiltinTypeHints'])) {
40 1
                throw new \InvalidArgumentException(
41 1
                    "Option 'supportedBuiltinTypeHints' must be an array."
42
                );
43
            }
44 3
            $numericIndexCount = count(array_filter(
45 3
                $options['supportedBuiltinTypeHints'],
46 3
                (function ($val) {
47 3
                    return preg_match('/^0*(0|[1-9]\\d*)$/', $val);
48 3
                }),
49 3
                ARRAY_FILTER_USE_KEY
50
            ));
51 3
            foreach ($options['supportedBuiltinTypeHints'] as $key => $value) {
52 3
                $numericIndex = false;
0 ignored issues
show
Unused Code introduced by Loren Osborn
$numericIndex is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
53 3
                $typeHintName = $key;
54 3
                $validFor     = $value;
55 3
                if (preg_match('/^0*(0|[1-9]\\d*)$/', $key) &&
56 3
                    (intval($key) >= 0)                     &&
57 3
                    (intval($key) <  $numericIndexCount)
58
                ) {
59 1
                    $numericIndex = true;
0 ignored issues
show
Unused Code introduced by Loren Osborn
$numericIndex is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
60 1
                    $typeHintName = $value;
61 1
                    $validFor     = self::PARAMETER_TYPES|self::RETURN_TYPES;
62
                }
63 3
                if (!is_string($typeHintName) || !preg_match('/^\\w(?<!\\d)\\w*$/', $typeHintName)) {
64 2
                    throw new \InvalidArgumentException(
65 2
                        sprintf(
66
                            "Option 'supportedBuiltinTypeHints's element %s " .
67 2
                                "isn't a valid typehint string.",
68 2
                            var_export($typeHintName, true)
69
                        )
70
                    );
71 1
                } elseif (!is_scalar($validFor)                                                                   ||
72 1
                    (strval($validFor) != strval(intval($validFor)))                                        ||
73 1
                    (intval($validFor) != (intval($validFor) & (self::PARAMETER_TYPES|self::RETURN_TYPES))) ||
74 1
                    (intval($validFor) == 0)
75
                ) {
76 1
                    throw new \InvalidArgumentException(
77 1
                        sprintf(
78 1
                            "Option 'supportedBuiltinTypeHints's %s typehint applies to invalid mask %s. Mask must be one of: %s::PARAMETER_TYPES (%d), %s::RETURN_TYPES (%d) or %s::PARAMETER_TYPES|%s::RETURN_TYPES (%d)",
79 1
                            var_export($typeHintName, true),
80 1
                            var_export($validFor, true),
81 1
                            self::class,
82 1
                            self::PARAMETER_TYPES,
83 1
                            self::class,
84 1
                            self::RETURN_TYPES,
85 1
                            self::class,
86 1
                            self::class,
87 1
                            (self::PARAMETER_TYPES | self::RETURN_TYPES)
88
                        )
89
                    );
90
                } else {
91
                    $this->supportedBuiltinTypeHints[$typeHintName] = intval($validFor) & (self::PARAMETER_TYPES|self::RETURN_TYPES);
92
                }
93
            }
94
        } else {
95 1973
            $phpVersionToSupport = PHP_VERSION_ID; // i.e. 50600
96 1973
            if (isset($options['phpVersionToSupport'])) {
97
                $formats = [
98
                    'version_id'     => '/^0*[57]\\d{4}$/',
99
                    'version_string' => '/^0*[57]((\\.\d{1,2}){1,2}(\\s*(alpha|beta|[a-z])(\\s*\\d*)))?$/',
100
                ];
101
                $matchingFormat = null;
102
                foreach ($formats as $formatName => $pattern) {
103
                    if (!$matchingFormat && preg_match($pattern, strval($options['phpVersionToSupport']))) {
0 ignored issues
show
Bug Best Practice introduced by Loren Osborn
The expression $matchingFormat of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
104
                        $matchingFormat = $formatName;
105
                    }
106
                }
107
                if (!$matchingFormat) {
0 ignored issues
show
Bug Best Practice introduced by Loren Osborn
The expression $matchingFormat of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
108
                    throw new \InvalidArgumentException(
109
                        sprintf(
110
                            "Option 'phpVersionToSupport' (%s) in unrecognized format.",
111
                            var_export(strval($options['phpVersionToSupport']), true)
112
                        )
113
                    );
114
                }
115
                if ($matchingFormat == 'version_string') {
116
                    $versionParts = explode('.', preg_replace('/\s*[a-z].*$/', '', strval($options['phpVersionToSupport'])));
117
                    $versionParts = array_slice(array_merge($versionParts, ['0', '0', '0']), 0, 3);
118
                    $phpVersionToSupport = 0;
119
                    foreach ($versionParts as $part) {
120
                        $phpVersionToSupport = intval($part) + (100 * $phpVersionToSupport);
121
                    }
122
                } else if ($matchingFormat == 'version_id') {
123
                    $phpVersionToSupport = intval(strval($options['phpVersionToSupport']));
124
                }
125
            }
126
            $builtInTypeNames = [
127
                'array'    => [
128 1973
                    'introduced_version' => 50100,
129 1973
                    'valid_for'          => self::PARAMETER_TYPES|self::RETURN_TYPES,
130
                ],
131
                'callable' => [
132 1973
                    'introduced_version' => 50400,
133 1973
                    'valid_for'          => self::PARAMETER_TYPES|self::RETURN_TYPES,
134
                ],
135
                'bool'     => [
136 1973
                    'introduced_version' => 70000,
137 1973
                    'valid_for'          => self::PARAMETER_TYPES|self::RETURN_TYPES,
138
                ],
139
                'float'    => [
140 1973
                    'introduced_version' => 70000,
141 1973
                    'valid_for'          => self::PARAMETER_TYPES|self::RETURN_TYPES,
142
                ],
143
                'int'      => [
144 1973
                    'introduced_version' => 70000,
145 1973
                    'valid_for'          => self::PARAMETER_TYPES|self::RETURN_TYPES,
146
                ],
147
                'string'   => [
148 1973
                    'introduced_version' => 70000,
149 1973
                    'valid_for'          => self::PARAMETER_TYPES|self::RETURN_TYPES,
150
                ],
151
                'iterable' => [
152 1973
                    'introduced_version' => 70100,
153 1973
                    'valid_for'          => self::PARAMETER_TYPES|self::RETURN_TYPES,
154
                ],
155
                'void'     => [
156 1973
                    'introduced_version' => 70100,
157 1973
                    'valid_for'          => self::RETURN_TYPES,
158
                ],
159
                'object'   => [
160 1973
                    'introduced_version' => 70200,
161 1973
                    'valid_for'          => self::PARAMETER_TYPES|self::RETURN_TYPES,
162
                ],
163
            ];
164 1973
            foreach ($builtInTypeNames as $typeHintName => $valid) {
165 1973
                if ($phpVersionToSupport >= $valid['introduced_version']) {
166 1973
                    $this->supportedBuiltinTypeHints[$typeHintName] = $valid['valid_for'];
167
                }
168
            }
169
        }
170 1973
    }
171
172 5
    public function enterNode(Node $node)
173
    {
174
        if (
175 5
            ($node instanceof Stmt\Function_)   ||
176 5
            ($node instanceof Stmt\ClassMethod) ||
177 5
            ($node instanceof Expr\Closure)
178
        ) {
179 4
            $this->fixSignature($node);
180
        }
181 5
    }
182
183
    /** @param Stmt\Function_|Stmt\ClassMethod|Expr\Closure $node */
184 4
    private function fixSignature($node)
185
    {
186 4
        foreach ($node->params as $param) {
187 2
            $param->type = $this->fixType($param->type, self::PARAMETER_TYPES);
188
        }
189 4
        $node->returnType = $this->fixType($node->returnType, self::RETURN_TYPES);
190 4
    }
191
192 4
    private function fixType($node, $contextType)
193
    {
194 4
        $typeAsString = $this->getTypeAsString($node);
195 4
        if ($typeAsString == '') {
196
            // $node === null
197 4
            return $node;
198
        }
199 2
        if ($node instanceof Node\NullableType) {
200
            $node->type = $this->fixType($node->type, $contextType);
201
            return $node;
202
        }
203 2
        $shouldBeBuiltInType = $this->isTypeDefinedForContext($typeAsString, $contextType);
204
        // This is the actual problem we found:
205
        //     'object' is being interperted as a builtin typehint
206
        //     but it isn't.
207 2
        if (is_string($node) && !$shouldBeBuiltInType) {
208
            return new Name($typeAsString);
209
        }
210
        // Just in case:
211
        //     This is the *OPPOSITE* of the issue we're seeing,
212
        //     where a builtin type could be recognized as a class
213
        //     name instead.
214 2
        if (($node instanceof Name) && $shouldBeBuiltInType) {
215
            return $typeAsString;
216
        }
217 2
        return $node;
218
    }
219
220 4
    private function getTypeAsString($node)
221
    {
222 4
        if (!is_null($node) && !is_string($node) && !($node instanceof Name) && !($node instanceof Node\NullableType)) {
223
            throw new \Exception(sprintf('LOGIC ERROR: %s doesn\'t look like a type. This shouldn\'t get called here.', var_export($node, true)));
224
        }
225 4
        if ($node instanceof Node\NullableType) {
226
            // This *SHOULD* never be called, but correct behavior shorter than Exception.
227
            return '?' . $this->getTypeAsString($node->type);
228
        }
229 4
        if ($node instanceof Name) {
230 2
            return ($node->isFullyQualified() ? '\\' : '') . $node->toString();
231
        }
232 4
        return strval($node);
233
    }
234
235 2
    private function isTypeDefinedForContext($typeName, $contextType)
236
    {
237
        return
238 2
            array_key_exists($typeName, $this->supportedBuiltinTypeHints) &&
239 2
            (($contextType & $this->supportedBuiltinTypeHints[$typeName]) == $contextType);
240
    }
241
}
242