Fix support for named closures

This commit is contained in:
Nicolas Grekas
2022-05-13 14:02:43 +02:00
parent ab36653962
commit 163f07400a
9 changed files with 78 additions and 91 deletions
+7 -37
View File
@@ -26,36 +26,23 @@ jobs:
- '7.4'
- '8.0'
- '8.1'
composer-options: ['']
experimental: [false]
steps:
- name: "Checkout code"
uses: actions/checkout@v2.3.3
uses: actions/checkout@v2
- name: "Install PHP with extensions"
uses: shivammathur/setup-php@2.7.0
uses: shivammathur/setup-php@v2
with:
coverage: "none"
php-version: ${{ matrix.php-version }}
ini-values: memory_limit=-1
tools: composer:v2
- name: "Add PHPUnit matcher"
run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
- name: "Set composer cache directory"
id: composer-cache
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
- name: "Cache composer"
uses: actions/cache@v2.1.2
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-${{ matrix.php-version }}-composer-${{ hashFiles('composer.json') }}
restore-keys: ${{ runner.os }}-${{ matrix.php-version }}-composer-
- run: composer install ${{ matrix.composer-options }}
- run: composer install
- name: "Install PHPUnit"
run: vendor/bin/simple-phpunit install
@@ -92,35 +79,22 @@ jobs:
- 'extra/markdown-extra'
- 'extra/string-extra'
- 'extra/twig-extra-bundle'
composer-options: ['']
experimental: [false]
steps:
- name: "Checkout code"
uses: actions/checkout@v2.3.3
uses: actions/checkout@v2
- name: "Install PHP with extensions"
uses: shivammathur/setup-php@2.7.0
uses: shivammathur/setup-php@v2
with:
coverage: "none"
php-version: ${{ matrix.php-version }}
ini-values: memory_limit=-1
tools: composer:v2
- name: "Add PHPUnit matcher"
run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
- name: "Set composer cache directory"
id: composer-cache
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
- name: "Cache composer"
uses: actions/cache@v2.1.2
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-${{ matrix.php-version }}-${{ matrix.extension }}-${{ hashFiles('composer.json') }}
restore-keys: ${{ runner.os }}-${{ matrix.php-version }}-${{ matrix.extension }}-
- run: composer install
- name: "Install PHPUnit"
@@ -129,10 +103,6 @@ jobs:
- name: "PHPUnit version"
run: vendor/bin/simple-phpunit --version
- if: matrix.extension == 'extra/markdown-extra' && matrix.php-version == '8.0'
working-directory: ${{ matrix.extension}}
run: composer config platform.php 7.4.99
- name: "Composer install"
working-directory: ${{ matrix.extension}}
run: composer install
@@ -158,10 +128,10 @@ jobs:
steps:
- name: "Checkout code"
uses: actions/checkout@v2.3.3
uses: actions/checkout@v2
- name: "Install PHP with extensions"
uses: shivammathur/setup-php@2.7.0
uses: shivammathur/setup-php@v2
with:
coverage: "none"
extensions: "gd, pdo_sqlite"
+2 -2
View File
@@ -30,8 +30,8 @@ You can tweak the output for the date part and the time part:
Supported values are: ``none``, ``short``, ``medium``, ``long``, and ``full``.
For greater flexibility, you can even define your own pattern (see the `ICU
user guide`_ for supported patterns).
For greater flexibility, you can even define your own pattern
(see the `ICU user guide`_ for supported patterns).
.. code-block:: twig
-2
View File
@@ -149,8 +149,6 @@ final class ExtensionSet
throw new \LogicException(sprintf('Unable to register extension "%s" as it is already registered.', $class));
}
// For BC/FC with namespaced aliases
$class = (new \ReflectionClass($class))->name;
$this->extensions[$class] = $extension;
}
+36 -47
View File
@@ -24,21 +24,21 @@ abstract class CallExpression extends AbstractExpression
{
$callable = $this->getAttribute('callable');
$closingParenthesis = false;
$isArray = false;
if (\is_string($callable) && false === strpos($callable, '::')) {
$compiler->raw($callable);
} else {
list($r, $callable) = $this->reflectCallable($callable);
if ($r instanceof \ReflectionMethod && \is_string($callable[0])) {
if ($r->isStatic()) {
[$r, $callable] = $this->reflectCallable($callable);
if (\is_string($callable)) {
$compiler->raw($callable);
} elseif (\is_array($callable) && \is_string($callable[0])) {
if (!$r instanceof \ReflectionMethod || $r->isStatic()) {
$compiler->raw(sprintf('%s::%s', $callable[0], $callable[1]));
} else {
$compiler->raw(sprintf('$this->env->getRuntime(\'%s\')->%s', $callable[0], $callable[1]));
}
} elseif ($r instanceof \ReflectionMethod && $callable[0] instanceof ExtensionInterface) {
// For BC/FC with namespaced aliases
$class = (new \ReflectionClass(\get_class($callable[0])))->name;
} elseif (\is_array($callable) && $callable[0] instanceof ExtensionInterface) {
$class = \get_class($callable[0]);
if (!$compiler->getEnvironment()->hasExtension($class)) {
// Compile a non-optimized call to trigger a \Twig\Error\RuntimeError, which cannot be a compile-time error
$compiler->raw(sprintf('$this->env->getExtension(\'%s\')', $class));
@@ -48,17 +48,11 @@ abstract class CallExpression extends AbstractExpression
$compiler->raw(sprintf('->%s', $callable[1]));
} else {
$closingParenthesis = true;
$isArray = true;
$compiler->raw(sprintf('call_user_func_array($this->env->get%s(\'%s\')->getCallable(), ', ucfirst($this->getAttribute('type')), $this->getAttribute('name')));
$compiler->raw(sprintf('$this->env->get%s(\'%s\')->getCallable()', ucfirst($this->getAttribute('type')), $this->getAttribute('name')));
}
}
$this->compileArguments($compiler, $isArray);
if ($closingParenthesis) {
$compiler->raw(')');
}
$this->compileArguments($compiler);
}
protected function compileArguments(Compiler $compiler, $isArray = false)
@@ -245,10 +239,7 @@ abstract class CallExpression extends AbstractExpression
private function getCallableParameters($callable, bool $isVariadic): array
{
list($r) = $this->reflectCallable($callable);
if (null === $r) {
return [[], false];
}
[$r, , $callableName] = $this->reflectCallable($callable);
$parameters = $r->getParameters();
if ($this->hasNode('node')) {
@@ -275,11 +266,6 @@ abstract class CallExpression extends AbstractExpression
array_pop($parameters);
$isPhpVariadic = true;
} else {
$callableName = $r->name;
if ($r instanceof \ReflectionMethod) {
$callableName = $r->getDeclaringClass()->name.'::'.$callableName;
}
throw new \LogicException(sprintf('The last parameter of "%s" for %s "%s" must be an array with default value, eg. "array $arg = []".', $callableName, $this->getAttribute('type'), $this->getAttribute('name')));
}
}
@@ -293,30 +279,33 @@ abstract class CallExpression extends AbstractExpression
return $this->reflector;
}
if (\is_array($callable)) {
if (!method_exists($callable[0], $callable[1])) {
// __call()
return [null, []];
}
$r = new \ReflectionMethod($callable[0], $callable[1]);
} elseif (\is_object($callable) && !$callable instanceof \Closure) {
$r = new \ReflectionObject($callable);
$r = $r->getMethod('__invoke');
$callable = [$callable, '__invoke'];
} elseif (\is_string($callable) && false !== $pos = strpos($callable, '::')) {
$class = substr($callable, 0, $pos);
$method = substr($callable, $pos + 2);
if (!method_exists($class, $method)) {
// __staticCall()
return [null, []];
}
$r = new \ReflectionMethod($callable);
$callable = [$class, $method];
} else {
$r = new \ReflectionFunction($callable);
if (\is_string($callable) && false !== $pos = strpos($callable, '::')) {
$callable = [substr($callable, 0, $pos), substr($callable, 2 + $pos)];
}
return $this->reflector = [$r, $callable];
if (\is_array($callable) && method_exists($callable[0], $callable[1])) {
$r = new \ReflectionMethod($callable[0], $callable[1]);
return $this->reflector = [$r, $callable, $r->class.'::'.$r->name];
}
$r = new \ReflectionFunction(\Closure::fromCallable($callable));
if (false !== strpos($r->name, '{closure}')) {
return $this->reflector = [$r, $callable, 'Closure'];
}
if ($object = $r->getClosureThis()) {
$callable = [$object, $r->name];
$callableName = (\function_exists('get_debug_type') ? get_debug_type($object) : \get_class($object)).'::'.$r->name;
} elseif ($class = $r->getClosureScopeClass()) {
$callable = [$class, $r->name];
$callableName = $class.'::'.$r->name;
} else {
$callable = $callableName = $r->name;
}
return $this->reflector = [$r, $callable, $callableName];
}
}
+2
View File
@@ -2,7 +2,9 @@
__call calls
--TEMPLATE--
{{ 'foo'|magic_call }}
{{ 'foo'|magic_call_closure }}
--DATA--
return []
--EXPECT--
magic_foo
magic_foo
+1
View File
@@ -168,6 +168,7 @@ class TwigTestExtension extends AbstractExtension
new TwigFilter('static_call_string', 'Twig\Tests\TwigTestExtension::staticCall'),
new TwigFilter('static_call_array', ['Twig\Tests\TwigTestExtension', 'staticCall']),
new TwigFilter('magic_call', [$this, 'magicCall']),
new TwigFilter('magic_call_closure', \Closure::fromCallable([$this, 'magicCall'])),
new TwigFilter('magic_call_string', 'Twig\Tests\TwigTestExtension::magicStaticCall'),
new TwigFilter('magic_call_array', ['Twig\Tests\TwigTestExtension', 'magicStaticCall']),
new TwigFilter('*_path', [$this, 'dynamic_path']),
+24 -1
View File
@@ -13,6 +13,7 @@ namespace Twig\Tests\Node\Expression;
use Twig\Environment;
use Twig\Error\SyntaxError;
use Twig\Extension\AbstractExtension;
use Twig\Loader\ArrayLoader;
use Twig\Loader\LoaderInterface;
use Twig\Node\Expression\ConstantExpression;
@@ -39,8 +40,23 @@ class FilterTest extends NodeTestCase
{
$environment = new Environment($this->createMock(LoaderInterface::class));
$environment->addFilter(new TwigFilter('bar', 'twig_tests_filter_dummy', ['needs_environment' => true]));
$environment->addFilter(new TwigFilter('bar_closure', \Closure::fromCallable(twig_tests_filter_dummy::class), ['needs_environment' => true]));
$environment->addFilter(new TwigFilter('barbar', 'Twig\Tests\Node\Expression\twig_tests_filter_barbar', ['needs_context' => true, 'is_variadic' => true]));
$extension = new class() extends AbstractExtension {
public function getFilters(): array
{
return [
new TwigFilter('foo', \Closure::fromCallable([$this, 'foo'])),
];
}
public function foo()
{
}
};
$environment->addExtension($extension);
$tests = [];
$expr = new ConstantExpression('foo', 1);
@@ -77,12 +93,15 @@ class FilterTest extends NodeTestCase
// filter as an anonymous function
$node = $this->createFilter(new ConstantExpression('foo', 1), 'anonymous');
$tests[] = [$node, 'call_user_func_array($this->env->getFilter(\'anonymous\')->getCallable(), ["foo"])'];
$tests[] = [$node, '$this->env->getFilter(\'anonymous\')->getCallable()("foo")'];
// needs environment
$node = $this->createFilter($string, 'bar');
$tests[] = [$node, 'twig_tests_filter_dummy($this->env, "abc")', $environment];
$node = $this->createFilter($string, 'bar_closure');
$tests[] = [$node, twig_tests_filter_dummy::class.'($this->env, "abc")', $environment];
$node = $this->createFilter($string, 'bar', [new ConstantExpression('bar', 1)]);
$tests[] = [$node, 'twig_tests_filter_dummy($this->env, "abc", "bar")', $environment];
@@ -104,6 +123,10 @@ class FilterTest extends NodeTestCase
]);
$tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_filter_barbar($context, "abc", "1", "2", [0 => "3", "foo" => "bar"])', $environment];
// from extension
$node = $this->createFilter($string, 'foo');
$tests[] = [$node, sprintf('$this->extensions[\'%s\']->foo("abc")', \get_class($extension)), $environment];
return $tests;
}
+5 -1
View File
@@ -36,6 +36,7 @@ class FunctionTest extends NodeTestCase
{
$environment = new Environment($this->createMock(LoaderInterface::class));
$environment->addFunction(new TwigFunction('foo', 'twig_tests_function_dummy', []));
$environment->addFunction(new TwigFunction('foo_closure', \Closure::fromCallable(twig_tests_function_dummy::class), []));
$environment->addFunction(new TwigFunction('bar', 'twig_tests_function_dummy', ['needs_environment' => true]));
$environment->addFunction(new TwigFunction('foofoo', 'twig_tests_function_dummy', ['needs_context' => true]));
$environment->addFunction(new TwigFunction('foobar', 'twig_tests_function_dummy', ['needs_environment' => true, 'needs_context' => true]));
@@ -46,6 +47,9 @@ class FunctionTest extends NodeTestCase
$node = $this->createFunction('foo');
$tests[] = [$node, 'twig_tests_function_dummy()', $environment];
$node = $this->createFunction('foo_closure');
$tests[] = [$node, twig_tests_function_dummy::class.'()', $environment];
$node = $this->createFunction('foo', [new ConstantExpression('bar', 1), new ConstantExpression('foobar', 1)]);
$tests[] = [$node, 'twig_tests_function_dummy("bar", "foobar")', $environment];
@@ -94,7 +98,7 @@ class FunctionTest extends NodeTestCase
// function as an anonymous function
$node = $this->createFunction('anonymous', [new ConstantExpression('foo', 1)]);
$tests[] = [$node, 'call_user_func_array($this->env->getFunction(\'anonymous\')->getCallable(), ["foo"])'];
$tests[] = [$node, '$this->env->getFunction(\'anonymous\')->getCallable()("foo")'];
return $tests;
}
+1 -1
View File
@@ -48,7 +48,7 @@ class TestTest extends NodeTestCase
// test as an anonymous function
$node = $this->createTest(new ConstantExpression('foo', 1), 'anonymous', [new ConstantExpression('foo', 1)]);
$tests[] = [$node, 'call_user_func_array($this->env->getTest(\'anonymous\')->getCallable(), ["foo", "foo"])'];
$tests[] = [$node, '$this->env->getTest(\'anonymous\')->getCallable()("foo", "foo")'];
// arbitrary named arguments
$string = new ConstantExpression('abc', 1);