diff --git a/src/Node/Expression/CallExpression.php b/src/Node/Expression/CallExpression.php index 79bc76094..406294a9a 100644 --- a/src/Node/Expression/CallExpression.php +++ b/src/Node/Expression/CallExpression.php @@ -13,6 +13,7 @@ namespace Twig\Node\Expression; use Twig\Compiler; use Twig\Extension\ExtensionInterface; +use Twig\TwigCallableInterface; use Twig\Util\CallableArgumentsExtractor; use Twig\Util\ReflectionCallable; @@ -33,7 +34,7 @@ abstract class CallExpression extends AbstractExpression if (\is_string($callable) && !str_contains($callable, '::')) { $compiler->raw($callable); } else { - $rc = $this->reflectCallable($callable); + $rc = $this->reflectCallable($twigCallable); $r = $rc->getReflector(); $callable = $rc->getCallable(); @@ -120,8 +121,8 @@ abstract class CallExpression extends AbstractExpression $compiler->raw(')'); } - private function reflectCallable($callable): ReflectionCallable + private function reflectCallable(TwigCallableInterface $callable): ReflectionCallable { - return $this->reflector ??= new ReflectionCallable($callable, $this->getAttribute('type'), $this->getAttribute('name')); + return $this->reflector ??= new ReflectionCallable($callable); } } diff --git a/src/Parser.php b/src/Parser.php index 22bb2d2aa..9189e1a23 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -176,7 +176,7 @@ class Parser if (null !== $test) { $e = new SyntaxError(\sprintf('Unexpected "%s" tag', $token->getValue()), $token->getLine(), $this->stream->getSourceContext()); - $callable = (new ReflectionCallable($test))->getCallable(); + $callable = (new ReflectionCallable(new TwigTest('decision', $test)))->getCallable(); if (\is_array($callable) && $callable[0] instanceof TokenParserInterface) { $e->appendMessage(\sprintf(' (expecting closing tag for the "%s" tag defined near line %s).', $callable[0]->getTag(), $lineno)); } diff --git a/src/TwigCallableInterface.php b/src/TwigCallableInterface.php index 13a10cd3b..2a8ff6116 100644 --- a/src/TwigCallableInterface.php +++ b/src/TwigCallableInterface.php @@ -18,6 +18,8 @@ interface TwigCallableInterface extends \Stringable { public function getName(): string; + public function getType(): string; + public function getDynamicName(): string; /** diff --git a/src/TwigFilter.php b/src/TwigFilter.php index 7eb66f713..70b1f8f3f 100644 --- a/src/TwigFilter.php +++ b/src/TwigFilter.php @@ -39,6 +39,11 @@ final class TwigFilter extends AbstractTwigCallable ], $this->options); } + public function getType(): string + { + return 'filter'; + } + public function getSafe(Node $filterArgs): ?array { if (null !== $this->options['is_safe']) { diff --git a/src/TwigFunction.php b/src/TwigFunction.php index d90a7e42c..4a10df95e 100644 --- a/src/TwigFunction.php +++ b/src/TwigFunction.php @@ -38,6 +38,11 @@ final class TwigFunction extends AbstractTwigCallable ], $this->options); } + public function getType(): string + { + return 'function'; + } + public function getParserCallable(): ?callable { return $this->options['parser_callable']; diff --git a/src/TwigTest.php b/src/TwigTest.php index 570ffd852..5e58ad8b0 100644 --- a/src/TwigTest.php +++ b/src/TwigTest.php @@ -35,6 +35,11 @@ final class TwigTest extends AbstractTwigCallable ], $this->options); } + public function getType(): string + { + return 'test'; + } + public function needsCharset(): bool { return false; diff --git a/src/Util/CallableArgumentsExtractor.php b/src/Util/CallableArgumentsExtractor.php index e8dea5217..8811ca9c8 100644 --- a/src/Util/CallableArgumentsExtractor.php +++ b/src/Util/CallableArgumentsExtractor.php @@ -17,9 +17,6 @@ use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\VariadicExpression; use Twig\Node\Node; use Twig\TwigCallableInterface; -use Twig\TwigFilter; -use Twig\TwigFunction; -use Twig\TwigTest; /** * @author Fabien Potencier @@ -28,20 +25,13 @@ use Twig\TwigTest; */ final class CallableArgumentsExtractor { - private string $type; - private string $name; + private ReflectionCallable $rc; public function __construct( private Node $node, private TwigCallableInterface $twigCallable, ) { - $this->type = match (true) { - $twigCallable instanceof TwigFunction => 'function', - $twigCallable instanceof TwigFilter => 'filter', - $twigCallable instanceof TwigTest => 'test', - default => throw new \LogicException('Unknown callable type.'), - }; - $this->name = $twigCallable->getName(); + $this->rc = new ReflectionCallable($twigCallable); } /** @@ -49,7 +39,6 @@ final class CallableArgumentsExtractor */ public function extractArguments(Node $arguments): array { - $rc = new ReflectionCallable($this->twigCallable->getCallable(), $this->type, $this->name); $extractedArguments = []; $named = false; foreach ($arguments as $name => $node) { @@ -57,7 +46,7 @@ final class CallableArgumentsExtractor $named = true; $name = $this->normalizeName($name); } elseif ($named) { - throw new SyntaxError(\sprintf('Positional arguments cannot be used after named arguments for %s "%s".', $this->type, $this->name), $this->node->getTemplateLine(), $this->node->getSourceContext()); + throw new SyntaxError(\sprintf('Positional arguments cannot be used after named arguments for %s "%s".', $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext()); } $extractedArguments[$name] = $node; @@ -65,8 +54,8 @@ final class CallableArgumentsExtractor if (!$named && !$this->twigCallable->isVariadic()) { $min = $this->twigCallable->getMinimalNumberOfRequiredArguments(); - if (\count($extractedArguments) < $rc->getReflector()->getNumberOfRequiredParameters() - $min) { - throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $rc->getReflector()->getParameters()[$min + \count($extractedArguments)]->getName(), $this->type, $this->name), $this->node->getTemplateLine(), $this->node->getSourceContext()); + if (\count($extractedArguments) < $this->rc->getReflector()->getNumberOfRequiredParameters() - $min) { + throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $this->rc->getReflector()->getParameters()[$min + \count($extractedArguments)]->getName(), $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext()); } return $extractedArguments; @@ -74,13 +63,13 @@ final class CallableArgumentsExtractor if (!$callable = $this->twigCallable->getCallable()) { if ($named) { - throw new SyntaxError(\sprintf('Named arguments are not supported for %s "%s".', $this->type, $this->name)); + throw new SyntaxError(\sprintf('Named arguments are not supported for %s "%s".', $this->twigCallable->getType(), $this->twigCallable->getName())); } - throw new SyntaxError(\sprintf('Arbitrary positional arguments are not supported for %s "%s".', $this->type, $this->name)); + throw new SyntaxError(\sprintf('Arbitrary positional arguments are not supported for %s "%s".', $this->twigCallable->getType(), $this->twigCallable->getName())); } - [$callableParameters, $isPhpVariadic] = $this->getCallableParameters($rc); + [$callableParameters, $isPhpVariadic] = $this->getCallableParameters(); $arguments = []; $names = []; $missingArguments = []; @@ -100,13 +89,13 @@ final class CallableArgumentsExtractor if (\array_key_exists($name, $extractedArguments)) { if (\array_key_exists($pos, $extractedArguments)) { - throw new SyntaxError(\sprintf('Argument "%s" is defined twice for %s "%s".', $name, $this->type, $this->name), $this->node->getTemplateLine(), $this->node->getSourceContext()); + throw new SyntaxError(\sprintf('Argument "%s" is defined twice for %s "%s".', $name, $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext()); } if (\count($missingArguments)) { throw new SyntaxError(\sprintf( '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".', - $name, $this->type, $this->name, implode(', ', $names), \count($missingArguments) > 1 ? 's' : '', implode('", "', $missingArguments) + $name, $this->twigCallable->getType(), $this->twigCallable->getName(), implode(', ', $names), \count($missingArguments) > 1 ? 's' : '', implode('", "', $missingArguments) ), $this->node->getTemplateLine(), $this->node->getSourceContext()); } @@ -129,7 +118,7 @@ final class CallableArgumentsExtractor $missingArguments[] = $name; } else { - throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $name, $this->type, $this->name), $this->node->getTemplateLine(), $this->node->getSourceContext()); + throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $name, $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext()); } } @@ -162,7 +151,7 @@ final class CallableArgumentsExtractor throw new SyntaxError( \sprintf( 'Unknown argument%s "%s" for %s "%s(%s)".', - \count($extractedArguments) > 1 ? 's' : '', implode('", "', array_keys($extractedArguments)), $this->type, $this->name, implode(', ', $names) + \count($extractedArguments) > 1 ? 's' : '', implode('", "', array_keys($extractedArguments)), $this->twigCallable->getType(), $this->twigCallable->getName(), implode(', ', $names) ), $unknownArgument ? $unknownArgument->getTemplateLine() : $this->node->getTemplateLine(), $unknownArgument ? $unknownArgument->getSourceContext() : $this->node->getSourceContext() @@ -177,11 +166,9 @@ final class CallableArgumentsExtractor return strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], ['\\1_\\2', '\\1_\\2'], $name)); } - private function getCallableParameters(ReflectionCallable $rc): array + private function getCallableParameters(): array { - $r = $rc->getReflector(); - - $parameters = $r->getParameters(); + $parameters = $this->rc->getReflector()->getParameters(); if ($this->node->hasNode('node')) { array_shift($parameters); } @@ -208,7 +195,7 @@ final class CallableArgumentsExtractor array_pop($parameters); $isPhpVariadic = true; } else { - throw new SyntaxError(\sprintf('The last parameter of "%s" for %s "%s" must be an array with default value, eg. "array $arg = []".', $rc->getName(), $this->type, $this->name)); + throw new SyntaxError(\sprintf('The last parameter of "%s" for %s "%s" must be an array with default value, eg. "array $arg = []".', $this->rc->getName(), $this->twigCallable->getType(), $this->twigCallable->getName())); } } diff --git a/src/Util/ReflectionCallable.php b/src/Util/ReflectionCallable.php index d7ce2474a..c37533150 100644 --- a/src/Util/ReflectionCallable.php +++ b/src/Util/ReflectionCallable.php @@ -11,6 +11,8 @@ namespace Twig\Util; +use Twig\TwigCallableInterface; + /** * @author Fabien Potencier * @@ -22,8 +24,10 @@ final class ReflectionCallable private \Closure|string|array|null $callable = null; private string $name; - public function __construct($callable, string $debugType = 'unknown', string $debugName = 'unknown') - { + public function __construct( + private TwigCallableInterface $twigCallable, + ) { + $callable = $twigCallable->getCallable(); if (\is_string($callable) && false !== $pos = strpos($callable, '::')) { $callable = [substr($callable, 0, $pos), substr($callable, 2 + $pos)]; } @@ -40,7 +44,7 @@ final class ReflectionCallable try { $closure = \Closure::fromCallable($callable); } catch (\TypeError $e) { - throw new \LogicException(\sprintf('Callback for %s "%s" is not callable in the current scope.', $debugType, $debugName), 0, $e); + throw new \LogicException(\sprintf('Callback for %s "%s" is not callable in the current scope.', $twigCallable->getType(), $twigCallable->getName()), 0, $e); } $this->reflector = $r = new \ReflectionFunction($closure); diff --git a/tests/Fixtures/macros/unknown_macro_different_template.test b/tests/Fixtures/macros/unknown_macro_different_template.test new file mode 100644 index 000000000..61604e8a9 --- /dev/null +++ b/tests/Fixtures/macros/unknown_macro_different_template.test @@ -0,0 +1,11 @@ +--TEST-- +Exception for unknown macro in different template +--TEMPLATE-- +{% import foo_template as macros %} +{{ macros.foo() }} +--TEMPLATE(foo.twig)-- +foo +--DATA-- +return array('foo_template' => 'foo.twig') +--EXCEPTION-- +Twig\Error\RuntimeError: Macro "foo" is not defined in template "foo.twig" in "index.twig" at line 3.