Completed
Push — master ( 296743...12eab9 )
by Kirill
36:17
created

ValueBuilder::toFloat()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
1
<?php
2
/**
3
 * This file is part of Railt package.
4
 *
5
 * For the full copyright and license information, please view the LICENSE
6
 * file that was distributed with this source code.
7
 */
8
declare(strict_types=1);
9
10
namespace Railt\SDL\Reflection\Builder\Invocations;
11
12
use Railt\Parser\Ast\LeafInterface;
13
use Railt\Parser\Ast\NodeInterface;
14
use Railt\Parser\Ast\RuleInterface;
15
use Railt\SDL\Contracts\Document;
16
use Railt\SDL\Contracts\Invocations\InputInvocation;
17
use Railt\SDL\Reflection\Builder\DocumentBuilder;
18
19
/**
20
 * Class ValueBuilder
21
 */
22
class ValueBuilder
23
{
24
    private const AST_ID_ARRAY  = 'List';
25
    private const AST_ID_OBJECT = 'Object';
26
27
    private const TOKEN_NULL       = 'T_NULL';
28
    private const TOKEN_NUMBER     = 'T_NUMBER_VALUE';
29
    private const TOKEN_BOOL_TRUE  = 'T_BOOL_TRUE';
30
    private const TOKEN_BOOL_FALSE = 'T_BOOL_FALSE';
31
32
    /**
33
     * @var Document|DocumentBuilder
34
     */
35
    private $document;
36
37
    /**
38
     * ValueBuilder constructor.
39
     * @param Document $document
40
     */
41 6576
    public function __construct(Document $document)
42
    {
43 6576
        $this->document = $document;
44 6576
    }
45
46
    /**
47
     * @param NodeInterface|RuleInterface|LeafInterface $ast
48
     * @param string $type
49
     * @param array $path
50
     * @return array|float|int|null|string
51
     */
52 5326
    public function parse(NodeInterface $ast, string $type, array $path = [])
53
    {
54 5326
        switch ($ast->getName()) {
55 5326
            case self::AST_ID_ARRAY:
56 2711
                return $this->toArray($ast, $type, $path);
57
58 5325
            case self::AST_ID_OBJECT:
59 798
                return $this->toObject($ast, $type, $path);
60
        }
61
62 5325
        return $this->toScalar($ast);
0 ignored issues
show
Compatibility introduced by
$ast of type object<Railt\Parser\Ast\NodeInterface> is not a sub-type of object<Railt\Parser\Ast\LeafInterface>. It seems like you assume a child interface of the interface Railt\Parser\Ast\NodeInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
63
    }
64
65
    /**
66
     * @param NodeInterface|RuleInterface $ast
67
     * @param string $type
68
     * @param array $path
69
     * @return array
70
     */
71 2711
    private function toArray(NodeInterface $ast, string $type, array $path): array
72
    {
73 2711
        $result = [];
74
75 2711
        foreach ($ast->getChildren() as $child) {
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Railt\Parser\Ast\NodeInterface as the method getChildren() does only exist in the following implementations of said interface: Railt\Compiler\Grammar\Delegate\IncludeDelegate, Railt\Compiler\Grammar\Delegate\RuleDelegate, Railt\Compiler\Grammar\Delegate\TokenDelegate, Railt\Parser\Ast\Rule.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
76 2410
            $result[] = $this->parse($child->getChild(0), $type, $path);
77
        }
78
79 2711
        return $result;
80
    }
81
82
    /**
83
     * @param NodeInterface $ast
84
     * @param string $type
85
     * @param array $path
86
     * @return InputInvocation
87
     */
88 798
    private function toObject(NodeInterface $ast, string $type, array $path): InputInvocation
89
    {
90 798
        return new InputInvocationBuilder($ast, $this->document, $type, $path);
91
    }
92
93
    /**
94
     * @param LeafInterface $ast
95
     * @return float|int|string|null
96
     */
97 5325
    private function toScalar(LeafInterface $ast)
98
    {
99 5325
        switch ($ast->getName()) {
100 5325
            case self::TOKEN_NUMBER:
101 5300
                if (\strpos($ast->getValue(), '.') !== false) {
102 348
                    return $this->toFloat($ast);
103
                }
104
105 5300
                return $this->toInt($ast);
106
107 3920
            case self::TOKEN_NULL:
108 3007
                return null;
109
110 1417
            case self::TOKEN_BOOL_TRUE:
111 96
                return true;
112
113 1321
            case self::TOKEN_BOOL_FALSE:
114 96
                return false;
115
        }
116
117 1225
        return $this->toString($ast);
118
    }
119
120
    /**
121
     * @param LeafInterface $ast
122
     * @return float
123
     */
124 348
    private function toFloat(LeafInterface $ast): float
125
    {
126 348
        return (float)$ast->getValue();
127
    }
128
129
    /**
130
     * @param LeafInterface $ast
131
     * @return int
132
     */
133 5300
    private function toInt(LeafInterface $ast): int
134
    {
135 5300
        return (int)$ast->getValue();
136
    }
137
138
    /**
139
     * @param LeafInterface $ast
140
     * @return string
141
     */
142 1225
    private function toString(LeafInterface $ast): string
143
    {
144 1225
        $result = $this->unpackStringData($ast);
145
146
        // Transform utf char \uXXXX -> X
147 1225
        $result = $this->renderUtfSequences($result);
148
149
        // Transform special chars
150 1225
        $result = $this->renderSpecialCharacters($result);
151
152
        // Unescape slashes "Some\\Any" => "Some\Any"
153 1225
        $result = \stripcslashes($result);
154
155 1225
        return $result;
156
    }
157
158
    /**
159
     * @param LeafInterface $ast
160
     * @return string
161
     */
162 1225
    private function unpackStringData(LeafInterface $ast): string
163
    {
164
        switch (true) {
165 1225
            case $ast->is('T_STRING'):
166 1033
                return \substr($ast->getValue(), 1, -1);
167 192
            case $ast->is('T_MULTILINE_STRING'):
168 96
                return \substr($ast->getValue(), 3, -3);
169
        }
170
171 96
        return (string)$ast->getValue();
172
    }
173
174
    /**
175
     * Method for parsing special control characters.
176
     *
177
     * @see http://facebook.github.io/graphql/October2016/#sec-String-Value
178
     *
179
     * @param string $body
180
     * @return string
181
     */
182 1225
    private function renderSpecialCharacters(string $body): string
183
    {
184
        // TODO Probably may be escaped by backslash like "\\n".
185 1225
        $source = ['\b', '\f', '\n', '\r', '\t'];
186 1225
        $out    = ["\u{0008}", "\u{000C}", "\u{000A}", "\u{000D}", "\u {0009}"];
187
188 1225
        return \str_replace($source, $out, $body);
189
    }
190
191
    /**
192
     * Method for parsing and decode utf-8 character
193
     * sequences like "\uXXXX" type.
194
     *
195
     * @see http://facebook.github.io/graphql/October2016/#sec-String-Value
196
     * @param string $body
197
     * @return string
198
     */
199 1225
    private function renderUtfSequences(string $body): string
200
    {
201
        // TODO Probably may be escaped by backslash like "\\u0000"
202 1225
        $pattern = '/\\\\u([0-9a-fA-F]{4})/';
203
204 1225
        $callee = function (array $matches): string {
205
            [$char, $code] = [$matches[0], $matches[1]];
0 ignored issues
show
Bug introduced by
The variable $char does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $code does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
206
207
            try {
208
                $rendered = \pack('H*', $code);
209
210
                return \mb_convert_encoding($rendered, 'UTF-8', 'UCS-2BE');
211
            } catch (\Error | \ErrorException $error) {
0 ignored issues
show
Bug introduced by
The class ErrorException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
212
                // Fallback?
213
                return $char;
214
            }
215 1225
        };
216
217 1225
        return @\preg_replace_callback($pattern, $callee, $body) ?? $body;
218
    }
219
}
220