CheckTypeDeclarationsPass::getExpressionLanguage()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
/*
4
 * This file is part of the Symfony package.
5
 *
6
 * (c) Fabien Potencier <[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 Symfony\Component\DependencyInjection\Compiler;
13
14
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
15
use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
16
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
17
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
18
use Symfony\Component\DependencyInjection\Container;
19
use Symfony\Component\DependencyInjection\Definition;
20
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
21
use Symfony\Component\DependencyInjection\Exception\InvalidParameterTypeException;
22
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
23
use Symfony\Component\DependencyInjection\ExpressionLanguage;
24
use Symfony\Component\DependencyInjection\Parameter;
25
use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag;
26
use Symfony\Component\DependencyInjection\Reference;
27
use Symfony\Component\DependencyInjection\ServiceLocator;
28
use Symfony\Component\ExpressionLanguage\Expression;
29
30
/**
31
 * Checks whether injected parameters are compatible with type declarations.
32
 *
33
 * This pass should be run after all optimization passes.
34
 *
35
 * It can be added either:
36
 *  * before removing passes to check all services even if they are not currently used,
37
 *  * after removing passes to check only services are used in the app.
38
 *
39
 * @author Nicolas Grekas <[email protected]>
40
 * @author Julien Maulny <[email protected]>
41
 */
42
final class CheckTypeDeclarationsPass extends AbstractRecursivePass
43
{
44
    protected bool $skipScalars = true;
45
46
    private const SCALAR_TYPES = [
47
        'int' => true,
48
        'float' => true,
49
        'bool' => true,
50
        'string' => true,
51
    ];
52
53
    private const BUILTIN_TYPES = [
54
        'array' => true,
55
        'bool' => true,
56
        'callable' => true,
57
        'float' => true,
58
        'int' => true,
59
        'iterable' => true,
60
        'object' => true,
61
        'string' => true,
62
    ];
63
64
    private ExpressionLanguage $expressionLanguage;
65
66
    /**
67
     * @param bool  $autoload   Whether services who's class in not loaded should be checked or not.
68
     *                          Defaults to false to save loading code during compilation.
69
     * @param array $skippedIds An array indexed by the service ids to skip
70
     */
71
    public function __construct(
72
        private bool $autoload = false,
73
        private array $skippedIds = [],
74
    ) {
75
    }
76
77
    protected function processValue(mixed $value, bool $isRoot = false): mixed
78
    {
79
        if (isset($this->skippedIds[$this->currentId])) {
80
            return $value;
81
        }
82
83
        if (!$value instanceof Definition || $value->hasErrors() || $value->isDeprecated()) {
84
            return parent::processValue($value, $isRoot);
85
        }
86
87
        if (!$this->autoload) {
88
            if (!$class = $value->getClass()) {
89
                return parent::processValue($value, $isRoot);
90
            }
91
            if (!class_exists($class, false) && !interface_exists($class, false)) {
92
                return parent::processValue($value, $isRoot);
93
            }
94
        }
95
96
        if (ServiceLocator::class === $value->getClass()) {
97
            return parent::processValue($value, $isRoot);
98
        }
99
100
        if ($constructor = $this->getConstructor($value, false)) {
101
            $this->checkTypeDeclarations($value, $constructor, $value->getArguments());
102
        }
103
104
        foreach ($value->getMethodCalls() as $methodCall) {
105
            try {
106
                $reflectionMethod = $this->getReflectionMethod($value, $methodCall[0]);
107
            } catch (RuntimeException $e) {
108
                if ($value->getFactory()) {
109
                    continue;
110
                }
111
112
                throw $e;
113
            }
114
115
            $this->checkTypeDeclarations($value, $reflectionMethod, $methodCall[1]);
116
        }
117
118
        return parent::processValue($value, $isRoot);
119
    }
120
121
    /**
122
     * @throws InvalidArgumentException When not enough parameters are defined for the method
123
     */
124
    private function checkTypeDeclarations(Definition $checkedDefinition, \ReflectionFunctionAbstract $reflectionFunction, array $values): void
125
    {
126
        $numberOfRequiredParameters = $reflectionFunction->getNumberOfRequiredParameters();
127
128
        if (\count($values) < $numberOfRequiredParameters) {
129
            throw new InvalidArgumentException(\sprintf('Invalid definition for service "%s": "%s::%s()" requires %d arguments, %d passed.', $this->currentId, $reflectionFunction->class, $reflectionFunction->name, $numberOfRequiredParameters, \count($values)));
130
        }
131
132
        $reflectionParameters = $reflectionFunction->getParameters();
133
        $checksCount = min($reflectionFunction->getNumberOfParameters(), \count($values));
134
135
        $envPlaceholderUniquePrefix = $this->container->getParameterBag() instanceof EnvPlaceholderParameterBag ? $this->container->getParameterBag()->getEnvPlaceholderUniquePrefix() : null;
0 ignored issues
show
Bug introduced by
The method getParameterBag() does not exist on null. ( Ignorable by Annotation )

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

135
        $envPlaceholderUniquePrefix = $this->container->/** @scrutinizer ignore-call */ getParameterBag() instanceof EnvPlaceholderParameterBag ? $this->container->getParameterBag()->getEnvPlaceholderUniquePrefix() : null;

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
136
137
        for ($i = 0; $i < $checksCount; ++$i) {
138
            $p = $reflectionParameters[$i];
139
            if (!$p->hasType() || $p->isVariadic()) {
140
                continue;
141
            }
142
            if (\array_key_exists($p->name, $values)) {
143
                $i = $p->name;
144
            } elseif (!\array_key_exists($i, $values)) {
145
                continue;
146
            }
147
148
            $this->checkType($checkedDefinition, $values[$i], $p, $envPlaceholderUniquePrefix);
149
        }
150
151
        if ($reflectionFunction->isVariadic() && ($lastParameter = end($reflectionParameters))->hasType()) {
152
            $variadicParameters = \array_slice($values, $lastParameter->getPosition());
153
154
            foreach ($variadicParameters as $variadicParameter) {
155
                $this->checkType($checkedDefinition, $variadicParameter, $lastParameter, $envPlaceholderUniquePrefix);
156
            }
157
        }
158
    }
159
160
    /**
161
     * @throws InvalidParameterTypeException When a parameter is not compatible with the declared type
162
     */
163
    private function checkType(Definition $checkedDefinition, mixed $value, \ReflectionParameter $parameter, ?string $envPlaceholderUniquePrefix, ?\ReflectionType $reflectionType = null): void
164
    {
165
        $reflectionType ??= $parameter->getType();
166
167
        if ($reflectionType instanceof \ReflectionUnionType) {
168
            foreach ($reflectionType->getTypes() as $t) {
169
                try {
170
                    $this->checkType($checkedDefinition, $value, $parameter, $envPlaceholderUniquePrefix, $t);
171
172
                    return;
173
                } catch (InvalidParameterTypeException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
174
                }
175
            }
176
177
            throw new InvalidParameterTypeException($this->currentId, $e->getCode(), $parameter);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $e seems to be defined by a foreach iteration on line 168. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
178
        }
179
        if ($reflectionType instanceof \ReflectionIntersectionType) {
0 ignored issues
show
Bug introduced by
The type ReflectionIntersectionType was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
180
            foreach ($reflectionType->getTypes() as $t) {
0 ignored issues
show
Bug introduced by
The method getTypes() does not exist on ReflectionType. It seems like you code against a sub-type of ReflectionType such as ReflectionUnionType. ( Ignorable by Annotation )

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

180
            foreach ($reflectionType->/** @scrutinizer ignore-call */ getTypes() as $t) {
Loading history...
181
                $this->checkType($checkedDefinition, $value, $parameter, $envPlaceholderUniquePrefix, $t);
182
            }
183
184
            return;
185
        }
186
        if (!$reflectionType instanceof \ReflectionNamedType) {
187
            return;
188
        }
189
190
        $type = $reflectionType->getName();
191
192
        if ($value instanceof Reference) {
193
            if (!$this->container->has($value = (string) $value)) {
194
                return;
195
            }
196
197
            if ('service_container' === $value && is_a($type, Container::class, true)) {
198
                return;
199
            }
200
201
            $value = $this->container->findDefinition($value);
202
        }
203
204
        if ('self' === $type) {
205
            $type = $parameter->getDeclaringClass()->getName();
206
        }
207
208
        if ('static' === $type) {
209
            $type = $checkedDefinition->getClass();
210
        }
211
212
        $class = null;
213
214
        if ($value instanceof Definition) {
215
            if ($value->hasErrors() || $value->getFactory()) {
216
                return;
217
            }
218
219
            $class = $value->getClass();
220
221
            if ($class && isset(self::BUILTIN_TYPES[strtolower($class)])) {
222
                $class = strtolower($class);
223
            } elseif (!$class || (!$this->autoload && !class_exists($class, false) && !interface_exists($class, false))) {
224
                return;
225
            }
226
        } elseif ($value instanceof Parameter) {
227
            $value = $this->container->getParameter($value);
228
        } elseif ($value instanceof Expression) {
229
            try {
230
                $value = $this->getExpressionLanguage()->evaluate($value, ['container' => $this->container]);
231
            } catch (\Exception) {
232
                // If a service from the expression cannot be fetched from the container, we skip the validation.
233
                return;
234
            }
235
        } elseif (\is_string($value)) {
236
            if ('%' === ($value[0] ?? '') && preg_match('/^%([^%]+)%$/', $value, $match)) {
237
                $value = $this->container->getParameter(substr($value, 1, -1));
238
            }
239
240
            if ($envPlaceholderUniquePrefix && \is_string($value) && str_contains($value, 'env_')) {
241
                // If the value is an env placeholder that is either mixed with a string or with another env placeholder, then its resolved value will always be a string, so we don't need to resolve it.
242
                // We don't need to change the value because it is already a string.
243
                if ('' === preg_replace('/'.$envPlaceholderUniquePrefix.'_\w+_[a-f0-9]{32}/U', '', $value, -1, $c) && 1 === $c) {
244
                    try {
245
                        $value = $this->container->resolveEnvPlaceholders($value, true);
246
                    } catch (\Exception) {
247
                        // If an env placeholder cannot be resolved, we skip the validation.
248
                        return;
249
                    }
250
                }
251
            }
252
        }
253
254
        if (null === $value && $parameter->allowsNull()) {
255
            return;
256
        }
257
258
        if (null === $class) {
259
            if ($value instanceof IteratorArgument) {
260
                $class = RewindableGenerator::class;
261
            } elseif ($value instanceof ServiceClosureArgument) {
262
                $class = \Closure::class;
263
            } elseif ($value instanceof ServiceLocatorArgument) {
264
                $class = ServiceLocator::class;
265
            } elseif (\is_object($value)) {
266
                $class = $value::class;
267
            } else {
268
                $class = \gettype($value);
269
                $class = ['integer' => 'int', 'double' => 'float', 'boolean' => 'bool'][$class] ?? $class;
270
            }
271
        }
272
273
        if (isset(self::SCALAR_TYPES[$type]) && isset(self::SCALAR_TYPES[$class])) {
274
            return;
275
        }
276
277
        if ('string' === $type && $class instanceof \Stringable) {
278
            return;
279
        }
280
281
        if ('callable' === $type && (\Closure::class === $class || method_exists($class, '__invoke'))) {
282
            return;
283
        }
284
285
        if ('callable' === $type && \is_array($value) && isset($value[0]) && ($value[0] instanceof Reference || $value[0] instanceof Definition || \is_string($value[0]))) {
286
            return;
287
        }
288
289
        if ('iterable' === $type && (\is_array($value) || 'array' === $class || is_subclass_of($class, \Traversable::class))) {
290
            return;
291
        }
292
293
        if ($type === $class) {
294
            return;
295
        }
296
297
        if ('object' === $type && !isset(self::BUILTIN_TYPES[$class])) {
298
            return;
299
        }
300
301
        if ('mixed' === $type) {
302
            return;
303
        }
304
305
        if (is_a($class, $type, true)) {
0 ignored issues
show
Bug introduced by
It seems like $type can also be of type null; however, parameter $class of is_a() 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

305
        if (is_a($class, /** @scrutinizer ignore-type */ $type, true)) {
Loading history...
306
            return;
307
        }
308
309
        if ('false' === $type) {
310
            if (false === $value) {
311
                return;
312
            }
313
        } elseif ('true' === $type) {
314
            if (true === $value) {
315
                return;
316
            }
317
        } elseif ($reflectionType->isBuiltin()) {
318
            $checkFunction = \sprintf('is_%s', $type);
319
            if ($checkFunction($value)) {
320
                return;
321
            }
322
        }
323
324
        throw new InvalidParameterTypeException($this->currentId, \is_object($value) ? $class : get_debug_type($value), $parameter);
0 ignored issues
show
Bug introduced by
It seems like $this->currentId can also be of type null; however, parameter $serviceId of Symfony\Component\Depend...xception::__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

324
        throw new InvalidParameterTypeException(/** @scrutinizer ignore-type */ $this->currentId, \is_object($value) ? $class : get_debug_type($value), $parameter);
Loading history...
Bug introduced by
It seems like is_object($value) ? $cla... get_debug_type($value) can also be of type null; however, parameter $type of Symfony\Component\Depend...xception::__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

324
        throw new InvalidParameterTypeException($this->currentId, /** @scrutinizer ignore-type */ \is_object($value) ? $class : get_debug_type($value), $parameter);
Loading history...
325
    }
326
327
    private function getExpressionLanguage(): ExpressionLanguage
328
    {
329
        return $this->expressionLanguage ??= new ExpressionLanguage(null, $this->container->getExpressionLanguageProviders());
330
    }
331
}
332