Complex classes like Twig_ExpressionParser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use Twig_ExpressionParser, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
23 | class Twig_ExpressionParser |
||
24 | { |
||
25 | const OPERATOR_LEFT = 1; |
||
26 | const OPERATOR_RIGHT = 2; |
||
27 | |||
28 | protected $parser; |
||
29 | protected $unaryOperators; |
||
30 | protected $binaryOperators; |
||
31 | |||
32 | public function __construct(Twig_Parser $parser, array $unaryOperators, array $binaryOperators) |
||
33 | { |
||
34 | $this->parser = $parser; |
||
35 | $this->unaryOperators = $unaryOperators; |
||
36 | $this->binaryOperators = $binaryOperators; |
||
37 | } |
||
38 | |||
39 | public function parseExpression($precedence = 0) |
||
40 | { |
||
41 | $expr = $this->getPrimary(); |
||
42 | $token = $this->parser->getCurrentToken(); |
||
43 | while ($this->isBinary($token) && $this->binaryOperators[$token->getValue()]['precedence'] >= $precedence) { |
||
44 | $op = $this->binaryOperators[$token->getValue()]; |
||
45 | $this->parser->getStream()->next(); |
||
46 | |||
47 | if (isset($op['callable'])) { |
||
48 | $expr = call_user_func($op['callable'], $this->parser, $expr); |
||
49 | } else { |
||
50 | $expr1 = $this->parseExpression(self::OPERATOR_LEFT === $op['associativity'] ? $op['precedence'] + 1 : $op['precedence']); |
||
51 | $class = $op['class']; |
||
52 | $expr = new $class($expr, $expr1, $token->getLine()); |
||
53 | } |
||
54 | |||
55 | $token = $this->parser->getCurrentToken(); |
||
56 | } |
||
57 | |||
58 | if (0 === $precedence) { |
||
59 | return $this->parseConditionalExpression($expr); |
||
60 | } |
||
61 | |||
62 | return $expr; |
||
63 | } |
||
64 | |||
65 | protected function getPrimary() |
||
|
|||
66 | { |
||
67 | $token = $this->parser->getCurrentToken(); |
||
68 | |||
69 | if ($this->isUnary($token)) { |
||
70 | $operator = $this->unaryOperators[$token->getValue()]; |
||
71 | $this->parser->getStream()->next(); |
||
72 | $expr = $this->parseExpression($operator['precedence']); |
||
73 | $class = $operator['class']; |
||
74 | |||
75 | return $this->parsePostfixExpression(new $class($expr, $token->getLine())); |
||
76 | } elseif ($token->test(Twig_Token::PUNCTUATION_TYPE, '(')) { |
||
77 | $this->parser->getStream()->next(); |
||
78 | $expr = $this->parseExpression(); |
||
79 | $this->parser->getStream()->expect(Twig_Token::PUNCTUATION_TYPE, ')', 'An opened parenthesis is not properly closed'); |
||
80 | |||
81 | return $this->parsePostfixExpression($expr); |
||
82 | } |
||
83 | |||
84 | return $this->parsePrimaryExpression(); |
||
85 | } |
||
86 | |||
87 | protected function parseConditionalExpression($expr) |
||
107 | |||
108 | protected function isUnary(Twig_Token $token) |
||
112 | |||
113 | protected function isBinary(Twig_Token $token) |
||
117 | |||
118 | public function parsePrimaryExpression() |
||
119 | { |
||
120 | $token = $this->parser->getCurrentToken(); |
||
121 | switch ($token->getType()) { |
||
122 | case Twig_Token::NAME_TYPE: |
||
123 | $this->parser->getStream()->next(); |
||
124 | switch ($token->getValue()) { |
||
125 | case 'true': |
||
126 | case 'TRUE': |
||
127 | $node = new Twig_Node_Expression_Constant(true, $token->getLine()); |
||
128 | break; |
||
129 | |||
130 | case 'false': |
||
131 | case 'FALSE': |
||
132 | $node = new Twig_Node_Expression_Constant(false, $token->getLine()); |
||
133 | break; |
||
134 | |||
135 | case 'none': |
||
136 | case 'NONE': |
||
137 | case 'null': |
||
138 | case 'NULL': |
||
139 | $node = new Twig_Node_Expression_Constant(null, $token->getLine()); |
||
140 | break; |
||
141 | |||
142 | default: |
||
143 | if ('(' === $this->parser->getCurrentToken()->getValue()) { |
||
144 | $node = $this->getFunctionNode($token->getValue(), $token->getLine()); |
||
145 | } else { |
||
146 | $node = new Twig_Node_Expression_Name($token->getValue(), $token->getLine()); |
||
147 | } |
||
148 | } |
||
149 | break; |
||
150 | |||
151 | case Twig_Token::NUMBER_TYPE: |
||
152 | $this->parser->getStream()->next(); |
||
153 | $node = new Twig_Node_Expression_Constant($token->getValue(), $token->getLine()); |
||
154 | break; |
||
155 | |||
156 | case Twig_Token::STRING_TYPE: |
||
157 | case Twig_Token::INTERPOLATION_START_TYPE: |
||
158 | $node = $this->parseStringExpression(); |
||
159 | break; |
||
160 | |||
161 | case Twig_Token::OPERATOR_TYPE: |
||
162 | if (preg_match(Twig_Lexer::REGEX_NAME, $token->getValue(), $matches) && $matches[0] == $token->getValue()) { |
||
163 | // in this context, string operators are variable names |
||
164 | $this->parser->getStream()->next(); |
||
165 | $node = new Twig_Node_Expression_Name($token->getValue(), $token->getLine()); |
||
166 | break; |
||
167 | } elseif (isset($this->unaryOperators[$token->getValue()])) { |
||
168 | $class = $this->unaryOperators[$token->getValue()]['class']; |
||
169 | |||
170 | $ref = new ReflectionClass($class); |
||
171 | $negClass = 'Twig_Node_Expression_Unary_Neg'; |
||
172 | $posClass = 'Twig_Node_Expression_Unary_Pos'; |
||
173 | if (!(in_array($ref->getName(), array($negClass, $posClass)) || $ref->isSubclassOf($negClass) || $ref->isSubclassOf($posClass))) { |
||
174 | throw new Twig_Error_Syntax(sprintf('Unexpected unary operator "%s"', $token->getValue()), $token->getLine(), $this->parser->getFilename()); |
||
175 | } |
||
176 | |||
177 | $this->parser->getStream()->next(); |
||
178 | $expr = $this->parsePrimaryExpression(); |
||
179 | |||
180 | $node = new $class($expr, $token->getLine()); |
||
181 | break; |
||
182 | } |
||
183 | |||
184 | default: |
||
185 | if ($token->test(Twig_Token::PUNCTUATION_TYPE, '[')) { |
||
186 | $node = $this->parseArrayExpression(); |
||
187 | } elseif ($token->test(Twig_Token::PUNCTUATION_TYPE, '{')) { |
||
188 | $node = $this->parseHashExpression(); |
||
189 | } else { |
||
190 | throw new Twig_Error_Syntax(sprintf('Unexpected token "%s" of value "%s"', Twig_Token::typeToEnglish($token->getType()), $token->getValue()), $token->getLine(), $this->parser->getFilename()); |
||
191 | } |
||
192 | } |
||
193 | |||
194 | return $this->parsePostfixExpression($node); |
||
195 | } |
||
196 | |||
197 | public function parseStringExpression() |
||
198 | { |
||
199 | $stream = $this->parser->getStream(); |
||
200 | |||
201 | $nodes = array(); |
||
202 | // a string cannot be followed by another string in a single expression |
||
203 | $nextCanBeString = true; |
||
204 | while (true) { |
||
205 | if ($nextCanBeString && $token = $stream->nextIf(Twig_Token::STRING_TYPE)) { |
||
206 | $nodes[] = new Twig_Node_Expression_Constant($token->getValue(), $token->getLine()); |
||
207 | $nextCanBeString = false; |
||
208 | } elseif ($stream->nextIf(Twig_Token::INTERPOLATION_START_TYPE)) { |
||
209 | $nodes[] = $this->parseExpression(); |
||
210 | $stream->expect(Twig_Token::INTERPOLATION_END_TYPE); |
||
211 | $nextCanBeString = true; |
||
212 | } else { |
||
213 | break; |
||
214 | } |
||
215 | } |
||
216 | |||
217 | $expr = array_shift($nodes); |
||
218 | foreach ($nodes as $node) { |
||
219 | $expr = new Twig_Node_Expression_Binary_Concat($expr, $node, $node->getLine()); |
||
220 | } |
||
221 | |||
222 | return $expr; |
||
223 | } |
||
224 | |||
225 | public function parseArrayExpression() |
||
226 | { |
||
227 | $stream = $this->parser->getStream(); |
||
228 | $stream->expect(Twig_Token::PUNCTUATION_TYPE, '[', 'An array element was expected'); |
||
229 | |||
230 | $node = new Twig_Node_Expression_Array(array(), $stream->getCurrent()->getLine()); |
||
231 | $first = true; |
||
232 | while (!$stream->test(Twig_Token::PUNCTUATION_TYPE, ']')) { |
||
233 | if (!$first) { |
||
234 | $stream->expect(Twig_Token::PUNCTUATION_TYPE, ',', 'An array element must be followed by a comma'); |
||
235 | |||
236 | // trailing ,? |
||
237 | if ($stream->test(Twig_Token::PUNCTUATION_TYPE, ']')) { |
||
238 | break; |
||
239 | } |
||
240 | } |
||
241 | $first = false; |
||
242 | |||
243 | $node->addElement($this->parseExpression()); |
||
244 | } |
||
245 | $stream->expect(Twig_Token::PUNCTUATION_TYPE, ']', 'An opened array is not properly closed'); |
||
246 | |||
247 | return $node; |
||
248 | } |
||
249 | |||
250 | public function parseHashExpression() |
||
251 | { |
||
252 | $stream = $this->parser->getStream(); |
||
253 | $stream->expect(Twig_Token::PUNCTUATION_TYPE, '{', 'A hash element was expected'); |
||
254 | |||
255 | $node = new Twig_Node_Expression_Array(array(), $stream->getCurrent()->getLine()); |
||
256 | $first = true; |
||
257 | while (!$stream->test(Twig_Token::PUNCTUATION_TYPE, '}')) { |
||
258 | if (!$first) { |
||
259 | $stream->expect(Twig_Token::PUNCTUATION_TYPE, ',', 'A hash value must be followed by a comma'); |
||
260 | |||
261 | // trailing ,? |
||
262 | if ($stream->test(Twig_Token::PUNCTUATION_TYPE, '}')) { |
||
263 | break; |
||
264 | } |
||
265 | } |
||
266 | $first = false; |
||
267 | |||
268 | // a hash key can be: |
||
269 | // |
||
270 | // * a number -- 12 |
||
271 | // * a string -- 'a' |
||
272 | // * a name, which is equivalent to a string -- a |
||
273 | // * an expression, which must be enclosed in parentheses -- (1 + 2) |
||
274 | if (($token = $stream->nextIf(Twig_Token::STRING_TYPE)) || ($token = $stream->nextIf(Twig_Token::NAME_TYPE)) || $token = $stream->nextIf(Twig_Token::NUMBER_TYPE)) { |
||
275 | $key = new Twig_Node_Expression_Constant($token->getValue(), $token->getLine()); |
||
276 | } elseif ($stream->test(Twig_Token::PUNCTUATION_TYPE, '(')) { |
||
277 | $key = $this->parseExpression(); |
||
278 | } else { |
||
279 | $current = $stream->getCurrent(); |
||
280 | |||
281 | throw new Twig_Error_Syntax(sprintf('A hash key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token "%s" of value "%s"', Twig_Token::typeToEnglish($current->getType()), $current->getValue()), $current->getLine(), $this->parser->getFilename()); |
||
282 | } |
||
283 | |||
284 | $stream->expect(Twig_Token::PUNCTUATION_TYPE, ':', 'A hash key must be followed by a colon (:)'); |
||
285 | $value = $this->parseExpression(); |
||
286 | |||
287 | $node->addElement($value, $key); |
||
288 | } |
||
289 | $stream->expect(Twig_Token::PUNCTUATION_TYPE, '}', 'An opened hash is not properly closed'); |
||
290 | |||
291 | return $node; |
||
292 | } |
||
293 | |||
294 | public function parsePostfixExpression($node) |
||
295 | { |
||
296 | while (true) { |
||
297 | $token = $this->parser->getCurrentToken(); |
||
298 | if ($token->getType() == Twig_Token::PUNCTUATION_TYPE) { |
||
299 | if ('.' == $token->getValue() || '[' == $token->getValue()) { |
||
300 | $node = $this->parseSubscriptExpression($node); |
||
301 | } elseif ('|' == $token->getValue()) { |
||
302 | $node = $this->parseFilterExpression($node); |
||
303 | } else { |
||
304 | break; |
||
305 | } |
||
306 | } else { |
||
307 | break; |
||
308 | } |
||
309 | } |
||
310 | |||
311 | return $node; |
||
312 | } |
||
313 | |||
314 | public function getFunctionNode($name, $line) |
||
315 | { |
||
316 | switch ($name) { |
||
317 | case 'parent': |
||
318 | $args = $this->parseArguments(); |
||
319 | if (!count($this->parser->getBlockStack())) { |
||
320 | throw new Twig_Error_Syntax('Calling "parent" outside a block is forbidden', $line, $this->parser->getFilename()); |
||
321 | } |
||
322 | |||
323 | if (!$this->parser->getParent() && !$this->parser->hasTraits()) { |
||
324 | throw new Twig_Error_Syntax('Calling "parent" on a template that does not extend nor "use" another template is forbidden', $line, $this->parser->getFilename()); |
||
325 | } |
||
326 | |||
327 | return new Twig_Node_Expression_Parent($this->parser->peekBlockStack(), $line); |
||
328 | case 'block': |
||
329 | return new Twig_Node_Expression_BlockReference($this->parseArguments()->getNode(0), false, $line); |
||
330 | case 'attribute': |
||
331 | $args = $this->parseArguments(); |
||
332 | if (count($args) < 2) { |
||
333 | throw new Twig_Error_Syntax('The "attribute" function takes at least two arguments (the variable and the attributes)', $line, $this->parser->getFilename()); |
||
334 | } |
||
335 | |||
336 | return new Twig_Node_Expression_GetAttr($args->getNode(0), $args->getNode(1), count($args) > 2 ? $args->getNode(2) : null, Twig_Template::ANY_CALL, $line); |
||
337 | default: |
||
338 | if (null !== $alias = $this->parser->getImportedSymbol('function', $name)) { |
||
339 | $arguments = new Twig_Node_Expression_Array(array(), $line); |
||
340 | foreach ($this->parseArguments() as $n) { |
||
341 | $arguments->addElement($n); |
||
342 | } |
||
343 | |||
344 | $node = new Twig_Node_Expression_MethodCall($alias['node'], $alias['name'], $arguments, $line); |
||
345 | $node->setAttribute('safe', true); |
||
346 | |||
347 | return $node; |
||
348 | } |
||
349 | |||
350 | $args = $this->parseArguments(true); |
||
351 | $class = $this->getFunctionNodeClass($name, $line); |
||
352 | |||
353 | return new $class($name, $args, $line); |
||
354 | } |
||
355 | } |
||
356 | |||
357 | public function parseSubscriptExpression($node) |
||
358 | { |
||
359 | $stream = $this->parser->getStream(); |
||
360 | $token = $stream->next(); |
||
361 | $lineno = $token->getLine(); |
||
362 | $arguments = new Twig_Node_Expression_Array(array(), $lineno); |
||
363 | $type = Twig_Template::ANY_CALL; |
||
364 | if ($token->getValue() == '.') { |
||
365 | $token = $stream->next(); |
||
366 | if ( |
||
367 | $token->getType() == Twig_Token::NAME_TYPE |
||
368 | || |
||
369 | $token->getType() == Twig_Token::NUMBER_TYPE |
||
370 | || |
||
371 | ($token->getType() == Twig_Token::OPERATOR_TYPE && preg_match(Twig_Lexer::REGEX_NAME, $token->getValue())) |
||
372 | ) { |
||
373 | $arg = new Twig_Node_Expression_Constant($token->getValue(), $lineno); |
||
374 | |||
375 | if ($stream->test(Twig_Token::PUNCTUATION_TYPE, '(')) { |
||
376 | $type = Twig_TemplateInterface::METHOD_CALL; |
||
377 | foreach ($this->parseArguments() as $n) { |
||
378 | $arguments->addElement($n); |
||
379 | } |
||
380 | } |
||
381 | } else { |
||
382 | throw new Twig_Error_Syntax('Expected name or number', $lineno, $this->parser->getFilename()); |
||
383 | } |
||
384 | |||
385 | if ($node instanceof Twig_Node_Expression_Name && null !== $this->parser->getImportedSymbol('template', $node->getAttribute('name'))) { |
||
386 | if (!$arg instanceof Twig_Node_Expression_Constant) { |
||
387 | throw new Twig_Error_Syntax(sprintf('Dynamic macro names are not supported (called on "%s")', $node->getAttribute('name')), $token->getLine(), $this->parser->getFilename()); |
||
388 | } |
||
389 | |||
390 | $node = new Twig_Node_Expression_MethodCall($node, 'get'.$arg->getAttribute('value'), $arguments, $lineno); |
||
391 | $node->setAttribute('safe', true); |
||
392 | |||
393 | return $node; |
||
394 | } |
||
395 | } else { |
||
396 | $type = Twig_Template::ARRAY_CALL; |
||
397 | |||
398 | // slice? |
||
399 | $slice = false; |
||
400 | if ($stream->test(Twig_Token::PUNCTUATION_TYPE, ':')) { |
||
401 | $slice = true; |
||
402 | $arg = new Twig_Node_Expression_Constant(0, $token->getLine()); |
||
403 | } else { |
||
404 | $arg = $this->parseExpression(); |
||
405 | } |
||
406 | |||
407 | if ($stream->nextIf(Twig_Token::PUNCTUATION_TYPE, ':')) { |
||
408 | $slice = true; |
||
409 | } |
||
410 | |||
411 | if ($slice) { |
||
412 | if ($stream->test(Twig_Token::PUNCTUATION_TYPE, ']')) { |
||
413 | $length = new Twig_Node_Expression_Constant(null, $token->getLine()); |
||
414 | } else { |
||
415 | $length = $this->parseExpression(); |
||
416 | } |
||
417 | |||
418 | $class = $this->getFilterNodeClass('slice', $token->getLine()); |
||
419 | $arguments = new Twig_Node(array($arg, $length)); |
||
420 | $filter = new $class($node, new Twig_Node_Expression_Constant('slice', $token->getLine()), $arguments, $token->getLine()); |
||
421 | |||
422 | $stream->expect(Twig_Token::PUNCTUATION_TYPE, ']'); |
||
423 | |||
424 | return $filter; |
||
425 | } |
||
426 | |||
427 | $stream->expect(Twig_Token::PUNCTUATION_TYPE, ']'); |
||
428 | } |
||
429 | |||
430 | return new Twig_Node_Expression_GetAttr($node, $arg, $arguments, $type, $lineno); |
||
431 | } |
||
432 | |||
433 | public function parseFilterExpression($node) |
||
434 | { |
||
435 | $this->parser->getStream()->next(); |
||
436 | |||
437 | return $this->parseFilterExpressionRaw($node); |
||
438 | } |
||
439 | |||
440 | public function parseFilterExpressionRaw($node, $tag = null) |
||
441 | { |
||
442 | while (true) { |
||
443 | $token = $this->parser->getStream()->expect(Twig_Token::NAME_TYPE); |
||
444 | |||
445 | $name = new Twig_Node_Expression_Constant($token->getValue(), $token->getLine()); |
||
446 | if (!$this->parser->getStream()->test(Twig_Token::PUNCTUATION_TYPE, '(')) { |
||
447 | $arguments = new Twig_Node(); |
||
448 | } else { |
||
449 | $arguments = $this->parseArguments(true); |
||
450 | } |
||
451 | |||
452 | $class = $this->getFilterNodeClass($name->getAttribute('value'), $token->getLine()); |
||
453 | |||
454 | $node = new $class($node, $name, $arguments, $token->getLine(), $tag); |
||
455 | |||
456 | if (!$this->parser->getStream()->test(Twig_Token::PUNCTUATION_TYPE, '|')) { |
||
457 | break; |
||
458 | } |
||
459 | |||
460 | $this->parser->getStream()->next(); |
||
461 | } |
||
462 | |||
463 | return $node; |
||
464 | } |
||
465 | |||
466 | /** |
||
467 | * Parses arguments. |
||
468 | * |
||
469 | * @param bool $namedArguments Whether to allow named arguments or not |
||
470 | * @param bool $definition Whether we are parsing arguments for a function definition |
||
471 | */ |
||
472 | public function parseArguments($namedArguments = false, $definition = false) |
||
473 | { |
||
474 | $args = array(); |
||
475 | $stream = $this->parser->getStream(); |
||
476 | |||
477 | $stream->expect(Twig_Token::PUNCTUATION_TYPE, '(', 'A list of arguments must begin with an opening parenthesis'); |
||
478 | while (!$stream->test(Twig_Token::PUNCTUATION_TYPE, ')')) { |
||
479 | if (!empty($args)) { |
||
480 | $stream->expect(Twig_Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma'); |
||
481 | } |
||
482 | |||
483 | if ($definition) { |
||
484 | $token = $stream->expect(Twig_Token::NAME_TYPE, null, 'An argument must be a name'); |
||
485 | $value = new Twig_Node_Expression_Name($token->getValue(), $this->parser->getCurrentToken()->getLine()); |
||
486 | } else { |
||
487 | $value = $this->parseExpression(); |
||
488 | } |
||
489 | |||
490 | $name = null; |
||
491 | if ($namedArguments && $token = $stream->nextIf(Twig_Token::OPERATOR_TYPE, '=')) { |
||
492 | if (!$value instanceof Twig_Node_Expression_Name) { |
||
493 | throw new Twig_Error_Syntax(sprintf('A parameter name must be a string, "%s" given', get_class($value)), $token->getLine(), $this->parser->getFilename()); |
||
494 | } |
||
495 | $name = $value->getAttribute('name'); |
||
496 | |||
497 | if ($definition) { |
||
498 | $value = $this->parsePrimaryExpression(); |
||
499 | |||
500 | if (!$this->checkConstantExpression($value)) { |
||
501 | throw new Twig_Error_Syntax(sprintf('A default value for an argument must be a constant (a boolean, a string, a number, or an array).'), $token->getLine(), $this->parser->getFilename()); |
||
502 | } |
||
503 | } else { |
||
504 | $value = $this->parseExpression(); |
||
505 | } |
||
506 | } |
||
507 | |||
508 | if ($definition) { |
||
509 | if (null === $name) { |
||
510 | $name = $value->getAttribute('name'); |
||
511 | $value = new Twig_Node_Expression_Constant(null, $this->parser->getCurrentToken()->getLine()); |
||
512 | } |
||
513 | $args[$name] = $value; |
||
514 | } else { |
||
515 | if (null === $name) { |
||
516 | $args[] = $value; |
||
517 | } else { |
||
518 | $args[$name] = $value; |
||
519 | } |
||
520 | } |
||
521 | } |
||
522 | $stream->expect(Twig_Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis'); |
||
523 | |||
524 | return new Twig_Node($args); |
||
525 | } |
||
526 | |||
527 | public function parseAssignmentExpression() |
||
544 | |||
545 | public function parseMultitargetExpression() |
||
546 | { |
||
547 | $targets = array(); |
||
548 | while (true) { |
||
549 | $targets[] = $this->parseExpression(); |
||
550 | if (!$this->parser->getStream()->nextIf(Twig_Token::PUNCTUATION_TYPE, ',')) { |
||
551 | break; |
||
552 | } |
||
553 | } |
||
554 | |||
555 | return new Twig_Node($targets); |
||
556 | } |
||
557 | |||
558 | protected function getFunctionNodeClass($name, $line) |
||
577 | |||
578 | protected function getFilterNodeClass($name, $line) |
||
597 | |||
598 | // checks that the node only contains "constant" elements |
||
599 | protected function checkConstantExpression(Twig_NodeInterface $node) |
||
615 | } |
||
616 |
Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a
@return
annotation as described here.