Test Failed
Push — master ( 6a1530...bb1f7e )
by Justin
04:13
created

Twig_Node_Expression_Call::normalizeName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
eloc 1
nc 1
nop 1
1
<?php
2
3
/*
4
 * This file is part of Twig.
5
 *
6
 * (c) Fabien Potencier
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
abstract class Twig_Node_Expression_Call extends Twig_Node_Expression
12
{
13
    private $reflector;
14
15
    protected function compileCallable(Twig_Compiler $compiler)
16
    {
17
        $callable = $this->getAttribute('callable');
18
19
        $closingParenthesis = false;
20
        if (is_string($callable) && false === strpos($callable, '::')) {
21
            $compiler->raw($callable);
22
        } else {
23
            list($r, $callable) = $this->reflectCallable($callable);
24
            if ($r instanceof ReflectionMethod && is_string($callable[0])) {
25
                if ($r->isStatic()) {
26
                    $compiler->raw(sprintf('%s::%s', $callable[0], $callable[1]));
27
                } else {
28
                    $compiler->raw(sprintf('$this->env->getRuntime(\'%s\')->%s', $callable[0], $callable[1]));
29
                }
30
            } elseif ($r instanceof ReflectionMethod && $callable[0] instanceof Twig_ExtensionInterface) {
31
                // For BC/FC with namespaced aliases
32
                $class = (new ReflectionClass(get_class($callable[0])))->name;
33
                if (!$compiler->getEnvironment()->hasExtension($class)) {
34
                    throw new Twig_Error_Runtime(sprintf('The "%s" extension is not enabled.', $class));
35
                }
36
37
                $compiler->raw(sprintf('$this->extensions[\'%s\']->%s', ltrim($class, '\\'), $callable[1]));
38
            } else {
39
                $closingParenthesis = true;
40
                $compiler->raw(sprintf('call_user_func_array($this->env->get%s(\'%s\')->getCallable(), array', ucfirst($this->getAttribute('type')), $this->getAttribute('name')));
41
            }
42
        }
43
44
        $this->compileArguments($compiler);
45
46
        if ($closingParenthesis) {
47
            $compiler->raw(')');
48
        }
49
    }
50
51
    protected function compileArguments(Twig_Compiler $compiler)
52
    {
53
        $compiler->raw('(');
54
55
        $first = true;
56
57
        if ($this->hasAttribute('needs_environment') && $this->getAttribute('needs_environment')) {
58
            $compiler->raw('$this->env');
59
            $first = false;
60
        }
61
62
        if ($this->hasAttribute('needs_context') && $this->getAttribute('needs_context')) {
63
            if (!$first) {
64
                $compiler->raw(', ');
65
            }
66
            $compiler->raw('$context');
67
            $first = false;
68
        }
69
70
        if ($this->hasAttribute('arguments')) {
71
            foreach ($this->getAttribute('arguments') as $argument) {
72
                if (!$first) {
73
                    $compiler->raw(', ');
74
                }
75
                $compiler->string($argument);
76
                $first = false;
77
            }
78
        }
79
80
        if ($this->hasNode('node')) {
81
            if (!$first) {
82
                $compiler->raw(', ');
83
            }
84
            $compiler->subcompile($this->getNode('node'));
85
            $first = false;
86
        }
87
88
        if ($this->hasNode('arguments')) {
89
            $callable = $this->getAttribute('callable');
90
            $arguments = $this->getArguments($callable, $this->getNode('arguments'));
91
            foreach ($arguments as $node) {
92
                if (!$first) {
93
                    $compiler->raw(', ');
94
                }
95
                $compiler->subcompile($node);
96
                $first = false;
97
            }
98
        }
99
100
        $compiler->raw(')');
101
    }
102
103
    protected function getArguments($callable = null, $arguments)
104
    {
105
        $callType = $this->getAttribute('type');
106
        $callName = $this->getAttribute('name');
107
108
        $parameters = array();
109
        $named = false;
110
        foreach ($arguments as $name => $node) {
111
            if (!is_int($name)) {
112
                $named = true;
113
                $name = $this->normalizeName($name);
114
            } elseif ($named) {
115
                throw new Twig_Error_Syntax(sprintf('Positional arguments cannot be used after named arguments for %s "%s".', $callType, $callName));
116
            }
117
118
            $parameters[$name] = $node;
119
        }
120
121
        $isVariadic = $this->hasAttribute('is_variadic') && $this->getAttribute('is_variadic');
122
        if (!$named && !$isVariadic) {
123
            return $parameters;
124
        }
125
126
        if (!$callable) {
127
            if ($named) {
128
                $message = sprintf('Named arguments are not supported for %s "%s".', $callType, $callName);
129
            } else {
130
                $message = sprintf('Arbitrary positional arguments are not supported for %s "%s".', $callType, $callName);
131
            }
132
133
            throw new LogicException($message);
134
        }
135
136
        $callableParameters = $this->getCallableParameters($callable, $isVariadic);
137
        $arguments = array();
138
        $names = array();
139
        $missingArguments = array();
140
        $optionalArguments = array();
141
        $pos = 0;
142
        foreach ($callableParameters as $callableParameter) {
143
            $names[] = $name = $this->normalizeName($callableParameter->name);
144
145
            if (array_key_exists($name, $parameters)) {
146
                if (array_key_exists($pos, $parameters)) {
147
                    throw new Twig_Error_Syntax(sprintf('Argument "%s" is defined twice for %s "%s".', $name, $callType, $callName));
148
                }
149
150
                if (count($missingArguments)) {
151
                    throw new Twig_Error_Syntax(sprintf(
152
                        'Argument "%s" could not be assigned for %s "%s(%s)" because it is mapped to an internal PHP function which cannot determine default value for optional argument%s "%s".',
153
                        $name, $callType, $callName, implode(', ', $names), count($missingArguments) > 1 ? 's' : '', implode('", "', $missingArguments))
154
                    );
155
                }
156
157
                $arguments = array_merge($arguments, $optionalArguments);
158
                $arguments[] = $parameters[$name];
159
                unset($parameters[$name]);
160
                $optionalArguments = array();
161
            } elseif (array_key_exists($pos, $parameters)) {
162
                $arguments = array_merge($arguments, $optionalArguments);
163
                $arguments[] = $parameters[$pos];
164
                unset($parameters[$pos]);
165
                $optionalArguments = array();
166
                ++$pos;
167
            } elseif ($callableParameter->isDefaultValueAvailable()) {
168
                $optionalArguments[] = new Twig_Node_Expression_Constant($callableParameter->getDefaultValue(), -1);
169
            } elseif ($callableParameter->isOptional()) {
170
                if (empty($parameters)) {
171
                    break;
172
                } else {
173
                    $missingArguments[] = $name;
174
                }
175
            } else {
176
                throw new Twig_Error_Syntax(sprintf('Value for argument "%s" is required for %s "%s".', $name, $callType, $callName));
177
            }
178
        }
179
180
        if ($isVariadic) {
181
            $arbitraryArguments = new Twig_Node_Expression_Array(array(), -1);
182
            foreach ($parameters as $key => $value) {
183
                if (is_int($key)) {
184
                    $arbitraryArguments->addElement($value);
185
                } else {
186
                    $arbitraryArguments->addElement($value, new Twig_Node_Expression_Constant($key, -1));
187
                }
188
                unset($parameters[$key]);
189
            }
190
191
            if ($arbitraryArguments->count()) {
192
                $arguments = array_merge($arguments, $optionalArguments);
193
                $arguments[] = $arbitraryArguments;
194
            }
195
        }
196
197
        if (!empty($parameters)) {
198
            $unknownParameter = null;
199
            foreach ($parameters as $parameter) {
200
                if ($parameter instanceof Twig_Node) {
201
                    $unknownParameter = $parameter;
202
                    break;
203
                }
204
            }
205
206
            throw new Twig_Error_Syntax(sprintf(
207
                'Unknown argument%s "%s" for %s "%s(%s)".',
208
                count($parameters) > 1 ? 's' : '', implode('", "', array_keys($parameters)), $callType, $callName, implode(', ', $names)
209
            ), $unknownParameter ? $unknownParameter->getTemplateLine() : -1);
210
        }
211
212
        return $arguments;
213
    }
214
215
    protected function normalizeName($name)
216
    {
217
        return strtolower(preg_replace(array('/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'), array('\\1_\\2', '\\1_\\2'), $name));
218
    }
219
220
    private function getCallableParameters($callable, $isVariadic)
221
    {
222
        list($r) = $this->reflectCallable($callable);
223
        if (null === $r) {
224
            return array();
225
        }
226
227
        $parameters = $r->getParameters();
228
        if ($this->hasNode('node')) {
229
            array_shift($parameters);
230
        }
231
        if ($this->hasAttribute('needs_environment') && $this->getAttribute('needs_environment')) {
232
            array_shift($parameters);
233
        }
234
        if ($this->hasAttribute('needs_context') && $this->getAttribute('needs_context')) {
235
            array_shift($parameters);
236
        }
237
        if ($this->hasAttribute('arguments') && null !== $this->getAttribute('arguments')) {
238
            foreach ($this->getAttribute('arguments') as $argument) {
239
                array_shift($parameters);
240
            }
241
        }
242
        if ($isVariadic) {
243
            $argument = end($parameters);
244
            if ($argument && $argument->isArray() && $argument->isDefaultValueAvailable() && array() === $argument->getDefaultValue()) {
245
                array_pop($parameters);
246
            } else {
247
                $callableName = $r->name;
248
                if ($r instanceof ReflectionMethod) {
249
                    $callableName = $r->getDeclaringClass()->name.'::'.$callableName;
250
                }
251
252
                throw new LogicException(sprintf('The last parameter of "%s" for %s "%s" must be an array with default value, eg. "array $arg = array()".', $callableName, $this->getAttribute('type'), $this->getAttribute('name')));
253
            }
254
        }
255
256
        return $parameters;
257
    }
258
259
    private function reflectCallable($callable)
260
    {
261
        if (null !== $this->reflector) {
262
            return $this->reflector;
263
        }
264
265
        if (is_array($callable)) {
266
            if (!method_exists($callable[0], $callable[1])) {
267
                // __call()
268
                return array(null, array());
269
            }
270
            $r = new ReflectionMethod($callable[0], $callable[1]);
271
        } elseif (is_object($callable) && !$callable instanceof Closure) {
272
            $r = new ReflectionObject($callable);
273
            $r = $r->getMethod('__invoke');
274
            $callable = array($callable, '__invoke');
275
        } elseif (is_string($callable) && false !== $pos = strpos($callable, '::')) {
276
            $class = substr($callable, 0, $pos);
277
            $method = substr($callable, $pos + 2);
278
            if (!method_exists($class, $method)) {
279
                // __staticCall()
280
                return array(null, array());
281
            }
282
            $r = new ReflectionMethod($callable);
0 ignored issues
show
Bug introduced by
The call to ReflectionMethod::__construct() has too few arguments starting with name. ( Ignorable by Annotation )

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

282
            $r = /** @scrutinizer ignore-call */ new ReflectionMethod($callable);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
283
            $callable = array($class, $method);
284
        } else {
285
            $r = new ReflectionFunction($callable);
286
        }
287
288
        return $this->reflector = array($r, $callable);
289
    }
290
}
291
292
class_alias('Twig_Node_Expression_Call', 'Twig\Node\Expression\CallExpression', false);
293