Completed
Push — master ( 8afd90...4bf010 )
by Kirill
03:48
created

ValueBuilder::extractScalar()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
cc 3
nc 3
nop 2
dl 0
loc 12
ccs 0
cts 11
cp 0
crap 12
rs 9.8666
c 0
b 0
f 0
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\Compiler\Builder\Common;
11
12
use Railt\Parser\Ast\LeafInterface;
13
use Railt\Parser\Ast\RuleInterface;
14
use Railt\Reflection\Contracts\Definition;
15
use Railt\Reflection\Contracts\Definition\Behaviour\ProvidesTypeIndication as TypeHint;
16
use Railt\Reflection\Contracts\Definition\TypeDefinition;
17
use Railt\Reflection\Definition\EnumDefinition;
18
use Railt\Reflection\Invocation\InputInvocation;
19
use Railt\Reflection\Type;
20
use Railt\SDL\Compiler\Ast\Value\ConstantValueNode;
21
use Railt\SDL\Compiler\Ast\Value\InputValueNode;
22
use Railt\SDL\Compiler\Ast\Value\ListValueNode;
23
use Railt\SDL\Compiler\Ast\Value\NullValueNode;
24
use Railt\SDL\Compiler\Ast\Value\ValueInterface;
25
use Railt\SDL\Compiler\Ast\Value\ValueNode;
26
use Railt\SDL\Exception\TypeConflictException;
27
28
/**
29
 * Class ValueBuilder
30
 */
31
class ValueBuilder
32
{
33
    /**
34
     * @var ValueTypeResolver
35
     */
36
    private $resolver;
37
38
    /**
39
     * @var Definition|TypeHint
40
     */
41
    private $type;
42
43
    /**
44
     * @param TypeHint|Definition $type
45
     */
46
    public function __construct(TypeHint $type)
47
    {
48
        $this->type = $type;
49
        $this->resolver = new ValueTypeResolver($this->type->getDocument()->getDictionary());
50
    }
51
52
    /**
53
     * @param ValueInterface $value
54
     * @return array|mixed
55
     * @throws \Railt\Io\Exception\ExternalFileException
56
     */
57
    public function valueOf(ValueInterface $value)
58
    {
59
        return $this->getValueOf($value, $this->type);
0 ignored issues
show
Bug introduced by
It seems like $this->type can also be of type object<Railt\Reflection\Contracts\Definition>; however, Railt\SDL\Compiler\Build...ueBuilder::getValueOf() does only seem to accept object<Railt\Reflection\...ProvidesTypeIndication>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
60
    }
61
62
    /**
63
     * @param ValueInterface $value
64
     * @param TypeHint $type
65
     * @return array|mixed
66
     * @throws \Railt\Io\Exception\ExternalFileException
67
     */
68
    private function getValueOf(ValueInterface $value, TypeHint $type)
69
    {
70
        if ($value instanceof ValueNode) {
71
            return $this->getValueOf($value->getInnerValue(), $type);
72
        }
73
74
        if ($this->type->isList()) {
0 ignored issues
show
Bug introduced by
The method isList does only exist in Railt\Reflection\Contrac...\ProvidesTypeIndication, but not in Railt\Reflection\Contracts\Definition.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
75
            return \iterator_to_array($this->valueOfList($type, $value));
76
        }
77
78
        return $this->valueOfNonList($type, $value);
79
    }
80
81
    /**
82
     * @param TypeHint|TypeDefinition $type
83
     * @param ValueInterface|ListValueNode $value
84
     * @return \Traversable
85
     * @throws \Railt\Io\Exception\ExternalFileException
86
     */
87
    private function valueOfList(TypeHint $type, ValueInterface $value): \Traversable
88
    {
89 View Code Duplication
        if ($value instanceof NullValueNode) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
90
            /**
91
             * @validation <name: Type! = null>
92
             */
93
            if ($type->isNonNull()) {
94
                $error = 'Non-Null type %s can not accept null value';
95
                throw (new TypeConflictException(\sprintf($error, $type)))->throwsIn($type->getFile(),
96
                    $value->getOffset());
97
            }
98
99
            return $value->toPrimitive();
100
        }
101
102
        /**
103
         * @validation <name: [Type] = Value>
104
         */
105
        if (! ($value instanceof ListValueNode)) {
106
            $error = 'Value of %s should be a List, but %s given';
107
            throw (new TypeConflictException(\sprintf($error, $type, $value->toString())))->throwsIn($type->getFile(),
108
                $value->getOffset());
109
        }
110
111
        foreach ($value->getValues() as $leaf) {
112
            /**
113
             * @validation <name: [Type!] = [null]>
114
             */
115
            if ($leaf instanceof NullValueNode) {
116
                if ($type->isListOfNonNulls()) {
117
                    $error = 'List of Non-Nulls %s can not accept null value';
118
                    throw (new TypeConflictException(\sprintf($error, $type)))->throwsIn($type->getFile(),
119
                        $leaf->getOffset());
120
                }
121
122
                yield $value->toPrimitive();
123
            } else {
124
                yield $this->extractValue($type, $leaf);
125
            }
126
        }
127
    }
128
129
    /**
130
     * @param TypeHint|TypeDefinition $type
131
     * @param ValueInterface|RuleInterface $value
132
     * @return mixed
133
     * @throws \Railt\Io\Exception\ExternalFileException
134
     */
135
    private function extractValue(TypeHint $type, ValueInterface $value)
136
    {
137
        $definition = $type->getDefinition();
138
139
        if ($definition::typeOf(Type::of(Type::INPUT_OBJECT))) {
140
            return $this->extractInput($type, $value);
141
        }
142
143
        if ($definition::typeOf(Type::of(Type::ENUM))) {
144
            return $this->extractEnum($type, $value);
145
        }
146
147
        if ($definition::typeOf(Type::of(Type::SCALAR))) {
148
            return $this->extractScalar($type, $value);
149
        }
150
151
        return $value->toPrimitive();
152
    }
153
154
    /**
155
     * @param TypeHint|TypeDefinition $type
156
     * @param ValueInterface|RuleInterface|InputValueNode $value
157
     * @return InputInvocation
158
     * @throws \Railt\Io\Exception\ExternalFileException
159
     */
160
    private function extractInput(TypeHint $type, ValueInterface $value): InputInvocation
161
    {
162
        /**
163
         * @validation <name: InputType = Value>
164
         */
165 View Code Duplication
        if (! ($value instanceof InputValueNode)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
166
            $error = 'Value of %s should be a %s, but %s given';
167
            throw (new TypeConflictException(\sprintf($error, $type, $type->getDefinition(),
168
                $value->toString())))->throwsIn($type->getFile(), $value->getOffset());
169
        }
170
171
        /** @var Definition\InputDefinition $definition */
172
        $definition = $type->getDefinition();
173
174
        $invocation = new InputInvocation($type->getDocument(), $definition->getName());
175
        $invocation->withOffset($value->getOffset());
176
177
        /**
178
         * @var LeafInterface $key
179
         * @var ValueInterface $child
180
         */
181
        foreach ($value->getValues() as $key => $child) {
182
            $name = $key->getValue();
0 ignored issues
show
Bug introduced by
The method getValue cannot be called on $key (of type integer|string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
183
184
            /**
185
             * @validation <name: InputType = {nonExistentField: Value}>
186
             */
187 View Code Duplication
            if (! $definition->hasField($name)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
188
                $error = 'Input field "%s" does not provided by %s, but %s given';
189
                throw (new TypeConflictException(\sprintf($error, $name, $type->getDefinition(),
190
                    $value->toString())))->throwsIn($type->getFile(), $value->getOffset());
191
            }
192
193
            $invocation->withArgument($name, $this->getValueOf($child, $definition->getField($name)));
0 ignored issues
show
Bug introduced by
It seems like $definition->getField($name) can be null; however, getValueOf() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
194
        }
195
196
        return $invocation;
197
    }
198
199
    /**
200
     * @param TypeHint|TypeDefinition|ConstantValueNode $type
201
     * @param ValueInterface|RuleInterface $value
202
     * @return mixed
203
     * @throws \Railt\Io\Exception\ExternalFileException
204
     */
205
    private function extractEnum(TypeHint $type, ValueInterface $value)
206
    {
207
        /**
208
         * @validation <name: Enum = "NotEnumValue">
209
         */
210 View Code Duplication
        if (! ($value instanceof ConstantValueNode)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
211
            $error = 'Value of %s can be one of %s value, but %s given';
212
            throw (new TypeConflictException(\sprintf($error, $type, $type->getDefinition(),
213
                $value->toString())))->throwsIn($type->getFile(), $value->getOffset());
214
        }
215
216
        /** @var EnumDefinition $definition */
217
        $definition = $type->getDefinition();
218
219
        $name = $value->toPrimitive();
220
221
        /**
222
         * @validation <name: Enum = NonExistentValue>
223
         */
224 View Code Duplication
        if (! $definition->hasValue($name)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
225
            $error = 'Enum %s does not provide value %s';
226
            throw (new TypeConflictException(\sprintf($error, $type->getDefinition(), $value->toString())))
227
                ->throwsIn($type->getFile(), $value->getOffset());
228
        }
229
230
        return $definition->getValue($name)->getValue();
231
    }
232
233
    /**
234
     * @param TypeHint|TypeDefinition $type
235
     * @param ValueInterface|RuleInterface $value
236
     * @return bool|float|int|mixed|null|string
237
     * @throws \Railt\Io\Exception\ExternalFileException
238
     * @throws \Railt\Reflection\Exception\TypeNotFoundException
239
     */
240
    private function extractScalar(TypeHint $type, ValueInterface $value)
241
    {
242
        if ($value instanceof NullValueNode) {
243
            return null;
244
        }
245
246
        try {
247
            return $this->resolver->castTo($type->getDefinition(), $value->toPrimitive(), $value->toString());
248
        } catch (TypeConflictException $e) {
249
            throw $e->throwsIn($type->getFile(), $value->getOffset());
250
        }
251
    }
252
253
    /**
254
     * @param TypeHint|TypeDefinition $type
255
     * @param ValueInterface $value
256
     * @return mixed
257
     * @throws \Railt\Io\Exception\ExternalFileException
258
     */
259
    private function valueOfNonList(TypeHint $type, ValueInterface $value)
260
    {
261
        /**
262
         * @validation <name: TypeHint = []>
263
         */
264
        if ($value instanceof ListValueNode) {
265
            $error = 'Value of %s should be a Non-List, but %s given';
266
            throw (new TypeConflictException(\sprintf($error, $type, $value->toString())))->throwsIn($type->getFile(),
267
                $value->getOffset());
268
        }
269
270
        /**
271
         * @validation <name: TypeHint! = null>
272
         */
273 View Code Duplication
        if ($value instanceof NullValueNode) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
274
            if ($type->isNonNull()) {
275
                $error = 'Non-Null type %s can not accept null value';
276
                throw (new TypeConflictException(\sprintf($error, $type)))->throwsIn($type->getFile(),
277
                    $value->getOffset());
278
            }
279
280
            return $value->toPrimitive();
281
        }
282
283
        return $this->extractValue($type, $value);
284
    }
285
}
286