From 797e49035654c9cb3bcbaef89057136c26286daf Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Wed, 4 Sep 2024 15:20:36 +0200 Subject: [PATCH 01/10] Improve exception expectations reliability --- tests/Cache/FilesystemTest.php | 18 ++++---- tests/CustomExtensionTest.php | 5 ++- tests/EnvironmentTest.php | 11 ++--- tests/ExpressionParserTest.php | 66 ++++++++++++++-------------- tests/Extension/SandboxTest.php | 13 +++--- tests/LexerTest.php | 19 ++++---- tests/Loader/ArrayTest.php | 9 ++-- tests/Loader/ChainTest.php | 6 +-- tests/Node/Expression/CallTest.php | 28 ++++++++---- tests/Node/Expression/FilterTest.php | 14 +++--- tests/ParserTest.php | 17 +++---- tests/TemplateTest.php | 19 ++++---- tests/TokenStreamTest.php | 14 +++--- 13 files changed, 128 insertions(+), 111 deletions(-) diff --git a/tests/Cache/FilesystemTest.php b/tests/Cache/FilesystemTest.php index dbb085b3f..e880562ff 100644 --- a/tests/Cache/FilesystemTest.php +++ b/tests/Cache/FilesystemTest.php @@ -81,9 +81,6 @@ class FilesystemTest extends TestCase public function testWriteFailMkdir() { - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('Unable to create the cache directory'); - if (\defined('PHP_WINDOWS_VERSION_BUILD')) { $this->markTestSkipped('Read-only directories not possible on Windows.'); } @@ -97,14 +94,14 @@ class FilesystemTest extends TestCase @mkdir($this->directory, 0555, true); $this->assertDirectoryExists($this->directory); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Unable to create the cache directory'); + $this->cache->write($key, $content); } public function testWriteFailDirWritable() { - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('Unable to write in the cache directory'); - if (\defined('PHP_WINDOWS_VERSION_BUILD')) { $this->markTestSkipped('Read-only directories not possible on Windows.'); } @@ -120,14 +117,14 @@ class FilesystemTest extends TestCase @mkdir($this->directory.'/cache', 0555); $this->assertDirectoryExists($this->directory.'/cache'); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Unable to write in the cache directory'); + $this->cache->write($key, $content); } public function testWriteFailWriteFile() { - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('Failed to write cache file'); - $key = $this->directory.'/cache/cachefile.php'; $content = $this->generateSource(); @@ -137,6 +134,9 @@ class FilesystemTest extends TestCase @mkdir($key, 0777, true); $this->assertDirectoryExists($key); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Failed to write cache file'); + $this->cache->write($key, $content); } diff --git a/tests/CustomExtensionTest.php b/tests/CustomExtensionTest.php index fab7813b1..40be3b382 100644 --- a/tests/CustomExtensionTest.php +++ b/tests/CustomExtensionTest.php @@ -23,11 +23,12 @@ class CustomExtensionTest extends TestCase */ public function testGetInvalidOperators(ExtensionInterface $extension, $expectedExceptionMessage) { + $env = new Environment(new ArrayLoader()); + $env->addExtension($extension); + $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage($expectedExceptionMessage); - $env = new Environment(new ArrayLoader()); - $env->addExtension($extension); $env->getUnaryOperators(); } diff --git a/tests/EnvironmentTest.php b/tests/EnvironmentTest.php index a5f082ecb..ff8dc741b 100644 --- a/tests/EnvironmentTest.php +++ b/tests/EnvironmentTest.php @@ -333,12 +333,12 @@ class EnvironmentTest extends TestCase public function testOverrideExtension() { + $twig = new Environment(new ArrayLoader()); + $twig->addExtension(new EnvironmentTest_Extension()); + $this->expectException(\LogicException::class); $this->expectExceptionMessage('Unable to register extension "Twig\Tests\EnvironmentTest_Extension" as it is already registered.'); - $twig = new Environment(new ArrayLoader()); - - $twig->addExtension(new EnvironmentTest_Extension()); $twig->addExtension(new EnvironmentTest_Extension()); } @@ -370,11 +370,12 @@ class EnvironmentTest extends TestCase public function testFailLoadTemplate() { + $template = 'testFailLoadTemplate.twig'; + $twig = new Environment(new ArrayLoader([$template => false])); + $this->expectException(RuntimeError::class); $this->expectExceptionMessage('Failed to load Twig template "testFailLoadTemplate.twig", index "112233": cache might be corrupted in "testFailLoadTemplate.twig".'); - $template = 'testFailLoadTemplate.twig'; - $twig = new Environment(new ArrayLoader([$template => false])); $twig->loadTemplate($twig->getTemplateClass($template), $template, 112233); } diff --git a/tests/ExpressionParserTest.php b/tests/ExpressionParserTest.php index 94d67033e..82ff47d44 100644 --- a/tests/ExpressionParserTest.php +++ b/tests/ExpressionParserTest.php @@ -41,10 +41,10 @@ class ExpressionParserTest extends TestCase */ public function testCanOnlyAssignToNames($template) { - $this->expectException(SyntaxError::class); - $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); $parser = new Parser($env); + + $this->expectException(SyntaxError::class); $parser->parse($env->tokenize(new Source($template, 'index'))); } @@ -84,10 +84,10 @@ class ExpressionParserTest extends TestCase */ public function testSequenceSyntaxError($template) { - $this->expectException(SyntaxError::class); - $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); $parser = new Parser($env); + + $this->expectException(SyntaxError::class); $parser->parse($env->tokenize(new Source($template, 'index'))); } @@ -210,12 +210,11 @@ class ExpressionParserTest extends TestCase public function testStringExpressionDoesNotConcatenateTwoConsecutiveStrings() { - $this->expectException(SyntaxError::class); - $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false, 'optimizations' => 0]); $stream = $env->tokenize(new Source('{{ "a" "b" }}', 'index')); $parser = new Parser($env); + $this->expectException(SyntaxError::class); $parser->parse($stream); } @@ -278,32 +277,30 @@ class ExpressionParserTest extends TestCase public function testAttributeCallDoesNotSupportNamedArguments() { - $this->expectException(SyntaxError::class); - $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); $parser = new Parser($env); + $this->expectException(SyntaxError::class); $parser->parse($env->tokenize(new Source('{{ foo.bar(name="Foo") }}', 'index'))); } public function testMacroCallDoesNotSupportNamedArguments() { - $this->expectException(SyntaxError::class); - $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); $parser = new Parser($env); + $this->expectException(SyntaxError::class); $parser->parse($env->tokenize(new Source('{% from _self import foo %}{% macro foo() %}{% endmacro %}{{ foo(name="Foo") }}', 'index'))); } public function testMacroDefinitionDoesNotSupportNonNameVariableName() { - $this->expectException(SyntaxError::class); - $this->expectExceptionMessage('An argument must be a name. Unexpected token "string" of value "a" ("name" expected) in "index" at line 1.'); - $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); $parser = new Parser($env); + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('An argument must be a name. Unexpected token "string" of value "a" ("name" expected) in "index" at line 1.'); + $parser->parse($env->tokenize(new Source('{% macro foo("a") %}{% endmacro %}', 'index'))); } @@ -312,12 +309,12 @@ class ExpressionParserTest extends TestCase */ public function testMacroDefinitionDoesNotSupportNonConstantDefaultValues($template) { - $this->expectException(SyntaxError::class); - $this->expectExceptionMessage('A default value for an argument must be a constant (a boolean, a string, a number, a sequence, or a mapping) in "index" at line 1'); - $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); $parser = new Parser($env); + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('A default value for an argument must be a constant (a boolean, a string, a number, a sequence, or a mapping) in "index" at line 1'); + $parser->parse($env->tokenize(new Source($template, 'index'))); } @@ -359,67 +356,68 @@ class ExpressionParserTest extends TestCase public function testUnknownFunction() { - $this->expectException(SyntaxError::class); - $this->expectExceptionMessage('Unknown "cycl" function. Did you mean "cycle" in "index" at line 1?'); - $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); $parser = new Parser($env); + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('Unknown "cycl" function. Did you mean "cycle" in "index" at line 1?'); + $parser->parse($env->tokenize(new Source('{{ cycl() }}', 'index'))); } public function testUnknownFunctionWithoutSuggestions() { - $this->expectException(SyntaxError::class); - $this->expectExceptionMessage('Unknown "foobar" function in "index" at line 1.'); - $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); $parser = new Parser($env); + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('Unknown "foobar" function in "index" at line 1.'); + $parser->parse($env->tokenize(new Source('{{ foobar() }}', 'index'))); } public function testUnknownFilter() { - $this->expectException(SyntaxError::class); - $this->expectExceptionMessage('Unknown "lowe" filter. Did you mean "lower" in "index" at line 1?'); - $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); $parser = new Parser($env); + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('Unknown "lowe" filter. Did you mean "lower" in "index" at line 1?'); + $parser->parse($env->tokenize(new Source('{{ 1|lowe }}', 'index'))); } public function testUnknownFilterWithoutSuggestions() { - $this->expectException(SyntaxError::class); - $this->expectExceptionMessage('Unknown "foobar" filter in "index" at line 1.'); - $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); $parser = new Parser($env); + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('Unknown "foobar" filter in "index" at line 1.'); + $parser->parse($env->tokenize(new Source('{{ 1|foobar }}', 'index'))); } public function testUnknownTest() { - $this->expectException(SyntaxError::class); - $this->expectExceptionMessage('Unknown "nul" test. Did you mean "null" in "index" at line 1'); - $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); $parser = new Parser($env); $stream = $env->tokenize(new Source('{{ 1 is nul }}', 'index')); + + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('Unknown "nul" test. Did you mean "null" in "index" at line 1'); + $parser->parse($stream); } public function testUnknownTestWithoutSuggestions() { - $this->expectException(SyntaxError::class); - $this->expectExceptionMessage('Unknown "foobar" test in "index" at line 1.'); - $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); $parser = new Parser($env); + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('Unknown "foobar" test in "index" at line 1.'); + $parser->parse($env->tokenize(new Source('{{ 1 is foobar }}', 'index'))); } diff --git a/tests/Extension/SandboxTest.php b/tests/Extension/SandboxTest.php index 810bbf74e..70052b859 100644 --- a/tests/Extension/SandboxTest.php +++ b/tests/Extension/SandboxTest.php @@ -14,6 +14,7 @@ namespace Twig\Tests\Extension; use PHPUnit\Framework\TestCase; use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Twig\Environment; +use Twig\Error\RuntimeError; use Twig\Error\SyntaxError; use Twig\Extension\SandboxExtension; use Twig\Extension\StringLoaderExtension; @@ -72,10 +73,11 @@ class SandboxTest extends TestCase */ public function testSandboxForCoreTags(string $tag, string $template) { + $twig = $this->getEnvironment(true, [], self::$templates, []); + $this->expectException(SecurityError::class); $this->expectExceptionMessageMatches(sprintf('/Tag "%s" is not allowed in "index \(string template .+?\)" at line 1/', $tag)); - $twig = $this->getEnvironment(true, [], self::$templates, []); $twig->createTemplate($template, 'index')->render([]); } @@ -124,10 +126,11 @@ class SandboxTest extends TestCase public function testSandboxWithInheritance() { + $twig = $this->getEnvironment(true, [], self::$templates, ['extends', 'block']); + $this->expectException(SecurityError::class); $this->expectExceptionMessage('Filter "json_encode" is not allowed in "1_child" at line 3.'); - $twig = $this->getEnvironment(true, [], self::$templates, ['extends', 'block']); $twig->load('1_child')->render([]); } @@ -441,14 +444,14 @@ EOF public function testSandboxWithNoClosureFilter() { - $this->expectException('\Twig\Error\RuntimeError'); - $this->expectExceptionMessage('The callable passed to the "filter" filter must be a Closure in sandbox mode in "index" at line 1.'); - $twig = $this->getEnvironment(true, ['autoescape' => 'html'], ['index' => <<expectException(RuntimeError::class); + $this->expectExceptionMessage('The callable passed to the "filter" filter must be a Closure in sandbox mode in "index" at line 1.'); + $twig->load('index')->render([]); } diff --git a/tests/LexerTest.php b/tests/LexerTest.php index 476373130..d8da55458 100644 --- a/tests/LexerTest.php +++ b/tests/LexerTest.php @@ -319,12 +319,12 @@ class LexerTest extends TestCase public function testStringWithUnterminatedInterpolation() { + $template = '{{ "bar #{x" }}'; + $lexer = new Lexer(new Environment(new ArrayLoader())); + $this->expectException(SyntaxError::class); $this->expectExceptionMessage('Unclosed """'); - $template = '{{ "bar #{x" }}'; - - $lexer = new Lexer(new Environment(new ArrayLoader())); $lexer->tokenize(new Source($template, 'index')); } @@ -388,9 +388,6 @@ class LexerTest extends TestCase public function testUnterminatedVariable() { - $this->expectException(SyntaxError::class); - $this->expectExceptionMessage('Unclosed "variable" in "index" at line 3'); - $template = ' {{ @@ -401,14 +398,14 @@ bar '; $lexer = new Lexer(new Environment(new ArrayLoader())); + + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('Unclosed "variable" in "index" at line 3'); $lexer->tokenize(new Source($template, 'index')); } public function testUnterminatedBlock() { - $this->expectException(SyntaxError::class); - $this->expectExceptionMessage('Unclosed "block" in "index" at line 3'); - $template = ' {% @@ -419,6 +416,10 @@ bar '; $lexer = new Lexer(new Environment(new ArrayLoader())); + + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('Unclosed "block" in "index" at line 3'); + $lexer->tokenize(new Source($template, 'index')); } diff --git a/tests/Loader/ArrayTest.php b/tests/Loader/ArrayTest.php index 543fe9ff6..478adf931 100644 --- a/tests/Loader/ArrayTest.php +++ b/tests/Loader/ArrayTest.php @@ -19,10 +19,9 @@ class ArrayTest extends TestCase { public function testGetSourceContextWhenTemplateDoesNotExist() { - $this->expectException(LoaderError::class); - $loader = new ArrayLoader(); + $this->expectException(LoaderError::class); $loader->getSourceContext('foo'); } @@ -57,10 +56,9 @@ class ArrayTest extends TestCase public function testGetCacheKeyWhenTemplateDoesNotExist() { - $this->expectException(LoaderError::class); - $loader = new ArrayLoader(); + $this->expectException(LoaderError::class); $loader->getCacheKey('foo'); } @@ -80,10 +78,9 @@ class ArrayTest extends TestCase public function testIsFreshWhenTemplateDoesNotExist() { - $this->expectException(LoaderError::class); - $loader = new ArrayLoader(); + $this->expectException(LoaderError::class); $loader->isFresh('foo', time()); } } diff --git a/tests/Loader/ChainTest.php b/tests/Loader/ChainTest.php index 52d6d4c72..16ccb9473 100644 --- a/tests/Loader/ChainTest.php +++ b/tests/Loader/ChainTest.php @@ -43,10 +43,9 @@ class ChainTest extends TestCase public function testGetSourceContextWhenTemplateDoesNotExist() { - $this->expectException(LoaderError::class); - $loader = new ChainLoader([]); + $this->expectException(LoaderError::class); $loader->getSourceContext('foo'); } @@ -63,10 +62,9 @@ class ChainTest extends TestCase public function testGetCacheKeyWhenTemplateDoesNotExist() { - $this->expectException(LoaderError::class); - $loader = new ChainLoader([]); + $this->expectException(LoaderError::class); $loader->getCacheKey('foo'); } diff --git a/tests/Node/Expression/CallTest.php b/tests/Node/Expression/CallTest.php index a1ea76fd4..75cac0547 100644 --- a/tests/Node/Expression/CallTest.php +++ b/tests/Node/Expression/CallTest.php @@ -30,37 +30,41 @@ class CallTest extends TestCase public function testGetArgumentsWhenPositionalArgumentsAfterNamedArguments() { + $node = $this->createFunctionExpression('date', 'date'); + $this->expectException(SyntaxError::class); $this->expectExceptionMessage('Positional arguments cannot be used after named arguments for function "date".'); - $node = $this->createFunctionExpression('date', 'date'); $this->getArguments($node, ['date', ['timestamp' => 123456, 'Y-m-d']]); } public function testGetArgumentsWhenArgumentIsDefinedTwice() { + $node = $this->createFunctionExpression('date', 'date'); + $this->expectException(SyntaxError::class); $this->expectExceptionMessage('Argument "format" is defined twice for function "date".'); - $node = $this->createFunctionExpression('date', 'date'); $this->getArguments($node, ['date', ['Y-m-d', 'format' => 'U']]); } public function testGetArgumentsWithWrongNamedArgumentName() { + $node = $this->createFunctionExpression('date', 'date'); + $this->expectException(SyntaxError::class); $this->expectExceptionMessage('Unknown argument "unknown" for function "date(format, timestamp)".'); - $node = $this->createFunctionExpression('date', 'date'); $this->getArguments($node, ['date', ['Y-m-d', 'timestamp' => null, 'unknown' => '']]); } public function testGetArgumentsWithWrongNamedArgumentNames() { + $node = $this->createFunctionExpression('date', 'date'); + $this->expectException(SyntaxError::class); $this->expectExceptionMessage('Unknown arguments "unknown1", "unknown2" for function "date(format, timestamp)".'); - $node = $this->createFunctionExpression('date', 'date'); $this->getArguments($node, ['date', ['Y-m-d', 'timestamp' => null, 'unknown1' => '', 'unknown2' => '']]); } @@ -70,10 +74,11 @@ class CallTest extends TestCase $this->markTestSkipped('substr_compare() has a default value in 8.0, so the test does not work anymore, one should find another PHP built-in function for this test to work in PHP 8.'); } + $node = $this->createFunctionExpression('substr_compare', 'substr_compare'); + $this->expectException(SyntaxError::class); $this->expectExceptionMessage('Argument "case_sensitivity" could not be assigned for function "substr_compare(main_str, str, offset, length, case_sensitivity)" because it is mapped to an internal PHP function which cannot determine default value for optional argument "length".'); - $node = $this->createFunctionExpression('substr_compare', 'substr_compare'); $this->getArguments($node, ['substr_compare', ['abcd', 'bc', 'offset' => 1, 'case_sensitivity' => true]]); } @@ -91,36 +96,41 @@ class CallTest extends TestCase public function testResolveArgumentsWithMissingParameterForArbitraryArguments() { + $node = $this->createFunctionExpression('foo', [$this, 'customFunctionWithArbitraryArguments'], true); + $this->expectException(\LogicException::class); $this->expectExceptionMessage('The last parameter of "Twig\\Tests\\Node\\Expression\\CallTest::customFunctionWithArbitraryArguments" for function "foo" must be an array with default value, eg. "array $arg = []".'); - $node = $this->createFunctionExpression('foo', [$this, 'customFunctionWithArbitraryArguments'], true); $this->getArguments($node, [[$this, 'customFunctionWithArbitraryArguments'], []]); } public function testGetArgumentsWithInvalidCallable() { + $node = $this->createFunctionExpression('foo', '', true); + $this->expectException(\LogicException::class); $this->expectExceptionMessage('Callback for function "foo" is not callable in the current scope.'); - $node = $this->createFunctionExpression('foo', '', true); + $this->getArguments($node, ['', []]); } public function testResolveArgumentsWithMissingParameterForArbitraryArgumentsOnFunction() { + $node = $this->createFunctionExpression('foo', 'Twig\Tests\Node\Expression\custom_call_test_function', true); + $this->expectException(\LogicException::class); $this->expectExceptionMessageMatches('#^The last parameter of "Twig\\\\Tests\\\\Node\\\\Expression\\\\custom_call_test_function" for function "foo" must be an array with default value, eg\\. "array \\$arg \\= \\[\\]"\\.$#'); - $node = $this->createFunctionExpression('foo', 'Twig\Tests\Node\Expression\custom_call_test_function', true); $this->getArguments($node, ['Twig\Tests\Node\Expression\custom_call_test_function', []]); } public function testResolveArgumentsWithMissingParameterForArbitraryArgumentsOnObject() { + $node = $this->createFunctionExpression('foo', new CallableTestClass(), true); + $this->expectException(\LogicException::class); $this->expectExceptionMessageMatches('#^The last parameter of "Twig\\\\Tests\\\\Node\\\\Expression\\\\CallableTestClass\\:\\:__invoke" for function "foo" must be an array with default value, eg\\. "array \\$arg \\= \\[\\]"\\.$#'); - $node = $this->createFunctionExpression('foo', new CallableTestClass(), true); $this->getArguments($node, [new CallableTestClass(), []]); } diff --git a/tests/Node/Expression/FilterTest.php b/tests/Node/Expression/FilterTest.php index ca792bc4d..1339b07dd 100644 --- a/tests/Node/Expression/FilterTest.php +++ b/tests/Node/Expression/FilterTest.php @@ -128,29 +128,31 @@ class FilterTest extends NodeTestCase public function testCompileWithWrongNamedArgumentName() { - $this->expectException(SyntaxError::class); - $this->expectExceptionMessage('Unknown argument "foobar" for filter "date(format, timezone)" at line 1.'); - $date = new ConstantExpression(0, 1); $node = $this->createFilter($this->getEnvironment(), $date, 'date', [ 'foobar' => new ConstantExpression('America/Chicago', 1), ]); $compiler = $this->getCompiler(); + + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('Unknown argument "foobar" for filter "date(format, timezone)" at line 1.'); + $compiler->compile($node); } public function testCompileWithMissingNamedArgument() { - $this->expectException(SyntaxError::class); - $this->expectExceptionMessage('Value for argument "from" is required for filter "replace" at line 1.'); - $value = new ConstantExpression(0, 1); $node = $this->createFilter($this->getEnvironment(), $value, 'replace', [ 'to' => new ConstantExpression('foo', 1), ]); $compiler = $this->getCompiler(); + + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('Value for argument "from" is required for filter "replace" at line 1.'); + $compiler->compile($node); } diff --git a/tests/ParserTest.php b/tests/ParserTest.php index 727092837..4d61c2341 100644 --- a/tests/ParserTest.php +++ b/tests/ParserTest.php @@ -29,9 +29,6 @@ class ParserTest extends TestCase { public function testUnknownTag() { - $this->expectException(SyntaxError::class); - $this->expectExceptionMessage('Unknown "foo" tag. Did you mean "for" at line 1?'); - $stream = new TokenStream([ new Token(Token::BLOCK_START_TYPE, '', 1), new Token(Token::NAME_TYPE, 'foo', 1), @@ -39,14 +36,15 @@ class ParserTest extends TestCase new Token(Token::EOF_TYPE, '', 1), ]); $parser = new Parser(new Environment(new ArrayLoader())); + + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('Unknown "foo" tag. Did you mean "for" at line 1?'); + $parser->parse($stream); } public function testUnknownTagWithoutSuggestions() { - $this->expectException(SyntaxError::class); - $this->expectExceptionMessage('Unknown "foobar" tag at line 1.'); - $stream = new TokenStream([ new Token(Token::BLOCK_START_TYPE, '', 1), new Token(Token::NAME_TYPE, 'foobar', 1), @@ -54,6 +52,10 @@ class ParserTest extends TestCase new Token(Token::EOF_TYPE, '', 1), ]); $parser = new Parser(new Environment(new ArrayLoader())); + + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('Unknown "foobar" tag at line 1.'); + $parser->parse($stream); } @@ -92,13 +94,12 @@ class ParserTest extends TestCase */ public function testFilterBodyNodesThrowsException($input) { - $this->expectException(SyntaxError::class); - $parser = $this->getParser(); $m = new \ReflectionMethod($parser, 'filterBodyNodes'); $m->setAccessible(true); + $this->expectException(SyntaxError::class); $m->invoke($parser, $input); } diff --git a/tests/TemplateTest.php b/tests/TemplateTest.php index 1f2a8d281..801701af2 100644 --- a/tests/TemplateTest.php +++ b/tests/TemplateTest.php @@ -27,10 +27,10 @@ class TemplateTest extends TestCase { public function testDisplayBlocksAcceptTemplateOnlyAsBlocks() { - $this->expectException(\LogicException::class); - $twig = new Environment(new ArrayLoader()); $template = new TemplateForTest($twig); + + $this->expectException(\LogicException::class); $template->displayBlock('foo', [], ['foo' => [new \stdClass(), 'foo']]); } @@ -143,31 +143,34 @@ class TemplateTest extends TestCase public function testRenderBlockWithUndefinedBlock() { + $twig = new Environment(new ArrayLoader()); + $template = new TemplateForTest($twig, 'index.twig'); + $this->expectException(RuntimeError::class); $this->expectExceptionMessage('Block "unknown" on template "index.twig" does not exist in "index.twig".'); - $twig = new Environment(new ArrayLoader()); - $template = new TemplateForTest($twig, 'index.twig'); $template->renderBlock('unknown', []); } public function testDisplayBlockWithUndefinedBlock() { + $twig = new Environment(new ArrayLoader()); + $template = new TemplateForTest($twig, 'index.twig'); + $this->expectException(RuntimeError::class); $this->expectExceptionMessage('Block "unknown" on template "index.twig" does not exist in "index.twig".'); - $twig = new Environment(new ArrayLoader()); - $template = new TemplateForTest($twig, 'index.twig'); $template->displayBlock('unknown', []); } public function testDisplayBlockWithUndefinedParentBlock() { + $twig = new Environment(new ArrayLoader()); + $template = new TemplateForTest($twig, 'parent.twig'); + $this->expectException(RuntimeError::class); $this->expectExceptionMessage('Block "foo" should not call parent() in "index.twig" as the block does not exist in the parent template "parent.twig"'); - $twig = new Environment(new ArrayLoader()); - $template = new TemplateForTest($twig, 'parent.twig'); $template->displayBlock('foo', [], ['foo' => [new TemplateForTest($twig, 'index.twig'), 'block_foo']], false); } diff --git a/tests/TokenStreamTest.php b/tests/TokenStreamTest.php index e8cb474b3..8f86ac87a 100644 --- a/tests/TokenStreamTest.php +++ b/tests/TokenStreamTest.php @@ -48,12 +48,13 @@ class TokenStreamTest extends TestCase public function testEndOfTemplateNext() { - $this->expectException(SyntaxError::class); - $this->expectExceptionMessage('Unexpected end of template'); - $stream = new TokenStream([ new Token(Token::BLOCK_START_TYPE, 1, 1), ]); + + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('Unexpected end of template'); + while (!$stream->isEOF()) { $stream->next(); } @@ -61,12 +62,13 @@ class TokenStreamTest extends TestCase public function testEndOfTemplateLook() { - $this->expectException(SyntaxError::class); - $this->expectExceptionMessage('Unexpected end of template'); - $stream = new TokenStream([ new Token(Token::BLOCK_START_TYPE, 1, 1), ]); + + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('Unexpected end of template'); + while (!$stream->isEOF()) { $stream->look(); $stream->next(); From 4cd8955f74fdecbb91b35cd1c898e08c0075dd53 Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Thu, 5 Sep 2024 09:24:38 +0200 Subject: [PATCH 02/10] Fix iterable return type Currently, the PHPDoc states that a string should be returned, but that's not correct. It can be anything that is echo-able. That means any scalar, Stringable and even null. --- src/Node/BlockNode.php | 2 +- src/Template.php | 8 ++++---- tests/Node/BlockTest.php | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Node/BlockNode.php b/src/Node/BlockNode.php index 3c06f155b..b4f939cf6 100644 --- a/src/Node/BlockNode.php +++ b/src/Node/BlockNode.php @@ -33,7 +33,7 @@ class BlockNode extends Node $compiler ->addDebugInfo($this) ->write("/**\n") - ->write(" * @return iterable\n") + ->write(" * @return iterable\n") ->write(" */\n") ->write(\sprintf("public function block_%s(array \$context, array \$blocks = []): iterable\n", $this->getAttribute('name')), "{\n") ->indent() diff --git a/src/Template.php b/src/Template.php index 2dc05686d..e6bf2486a 100644 --- a/src/Template.php +++ b/src/Template.php @@ -382,7 +382,7 @@ abstract class Template } /** - * @return iterable + * @return iterable */ public function yield(array $context, array $blocks = []): iterable { @@ -412,7 +412,7 @@ abstract class Template } /** - * @return iterable + * @return iterable */ public function yieldBlock($name, array $context, array $blocks = [], $useBlocks = true, ?self $templateContext = null): iterable { @@ -472,7 +472,7 @@ abstract class Template * @param array $context The context * @param array $blocks The current set of blocks * - * @return iterable + * @return iterable */ public function yieldParentBlock($name, array $context, array $blocks = []): iterable { @@ -491,7 +491,7 @@ abstract class Template * @param array $context An array of parameters to pass to the template * @param array $blocks An array of blocks to pass to the template * - * @return iterable + * @return iterable */ abstract protected function doDisplay(array $context, array $blocks = []): iterable; } diff --git a/tests/Node/BlockTest.php b/tests/Node/BlockTest.php index af110e32a..2304c8abd 100644 --- a/tests/Node/BlockTest.php +++ b/tests/Node/BlockTest.php @@ -34,7 +34,7 @@ class BlockTest extends NodeTestCase $tests[] = [new BlockNode('foo', new TextNode('foo', 1), 1), << + * @return iterable */ public function block_foo(array \$context, array \$blocks = []): iterable { From 2b887e64d5e823376d965e500604fc31e3c8aae6 Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Fri, 6 Sep 2024 09:42:11 +0200 Subject: [PATCH 03/10] Fix isset in ForLoopNode Even though the code works perfectly fine, PHPStan doesn't understand it. Since we want to work with `revindex` and `revindex0`, we can better check for their existence, instead of `length`. --- src/Node/ForLoopNode.php | 2 +- tests/Node/ForTest.php | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Node/ForLoopNode.php b/src/Node/ForLoopNode.php index 3e044bbb0..1f0a4f321 100644 --- a/src/Node/ForLoopNode.php +++ b/src/Node/ForLoopNode.php @@ -38,7 +38,7 @@ class ForLoopNode extends Node ->write("++\$context['loop']['index0'];\n") ->write("++\$context['loop']['index'];\n") ->write("\$context['loop']['first'] = false;\n") - ->write("if (isset(\$context['loop']['length'])) {\n") + ->write("if (isset(\$context['loop']['revindex0'], \$context['loop']['revindex'])) {\n") ->indent() ->write("--\$context['loop']['revindex0'];\n") ->write("--\$context['loop']['revindex'];\n") diff --git a/tests/Node/ForTest.php b/tests/Node/ForTest.php index c4c11253d..047c2a109 100644 --- a/tests/Node/ForTest.php +++ b/tests/Node/ForTest.php @@ -101,7 +101,7 @@ foreach (\$context['_seq'] as \$context["k"] => \$context["v"]) { ++\$context['loop']['index0']; ++\$context['loop']['index']; \$context['loop']['first'] = false; - if (isset(\$context['loop']['length'])) { + if (isset(\$context['loop']['revindex0'], \$context['loop']['revindex'])) { --\$context['loop']['revindex0']; --\$context['loop']['revindex']; \$context['loop']['last'] = 0 === \$context['loop']['revindex0']; @@ -143,7 +143,7 @@ foreach (\$context['_seq'] as \$context["k"] => \$context["v"]) { ++\$context['loop']['index0']; ++\$context['loop']['index']; \$context['loop']['first'] = false; - if (isset(\$context['loop']['length'])) { + if (isset(\$context['loop']['revindex0'], \$context['loop']['revindex'])) { --\$context['loop']['revindex0']; --\$context['loop']['revindex']; \$context['loop']['last'] = 0 === \$context['loop']['revindex0']; @@ -187,7 +187,7 @@ foreach (\$context['_seq'] as \$context["k"] => \$context["v"]) { ++\$context['loop']['index0']; ++\$context['loop']['index']; \$context['loop']['first'] = false; - if (isset(\$context['loop']['length'])) { + if (isset(\$context['loop']['revindex0'], \$context['loop']['revindex'])) { --\$context['loop']['revindex0']; --\$context['loop']['revindex']; \$context['loop']['last'] = 0 === \$context['loop']['revindex0']; From 2ae0c0d38c9042ed1bfcdef53c57eca46c0e7dd5 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 6 Sep 2024 12:33:38 +0200 Subject: [PATCH 04/10] Fix CS --- src/ExpressionParser.php | 2 +- src/Extension/CoreExtension.php | 2 +- src/Node/ImportNode.php | 6 +++--- src/Node/Node.php | 6 +++--- src/Template.php | 8 ++++---- src/Test/NodeTestCase.php | 2 +- src/TokenParser/TypesTokenParser.php | 4 ++-- tests/EnvironmentTest.php | 2 +- tests/Extension/CoreTest.php | 5 +++-- tests/Extension/SandboxTest.php | 8 ++++---- tests/Node/Expression/FilterTest.php | 2 +- tests/ParserTest.php | 2 +- tests/TemplateTest.php | 4 ++-- tests/TokenParser/TypesTokenParserTest.php | 8 ++++---- 14 files changed, 31 insertions(+), 30 deletions(-) diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 8cd8b194a..dc4a6015d 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -568,7 +568,7 @@ class ExpressionParser public function parseFilterExpressionRaw($node) { - if (func_num_args() > 1) { + if (\func_num_args() > 1) { trigger_deprecation('twig/twig', '3.12', 'Passing a second argument to "%s()" is deprecated.', __METHOD__); } diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 9dc6d6e6a..ee0a1dbaf 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -348,7 +348,7 @@ final class CoreExtension extends AbstractExtension throw new RuntimeError('The "cycle" function expects an array or "ArrayAccess" as first argument.'); } - if (!\is_countable($values)) { + if (!is_countable($values)) { // To be uncommented in 4.0 // throw new RuntimeError('The "cycle" function expects a countable sequence as first argument.'); diff --git a/src/Node/ImportNode.php b/src/Node/ImportNode.php index 31acbe9d1..9a6033f21 100644 --- a/src/Node/ImportNode.php +++ b/src/Node/ImportNode.php @@ -29,10 +29,10 @@ class ImportNode extends Node */ public function __construct(AbstractExpression $expr, AbstractExpression $var, int $lineno, $global = true) { - if (null === $global || is_string($global)) { + if (null === $global || \is_string($global)) { trigger_deprecation('twig/twig', '3.12', 'Passing a tag to %s() is deprecated.', __METHOD__); - $global = func_num_args() > 4 ? func_get_arg(4) : true; - } elseif (!is_bool($global)) { + $global = \func_num_args() > 4 ? func_get_arg(4) : true; + } elseif (!\is_bool($global)) { throw new \TypeError(\sprintf('Argument 4 passed to "%s()" must be a boolean, "%s" given.', __METHOD__, get_debug_type($global))); } diff --git a/src/Node/Node.php b/src/Node/Node.php index 38ebdfa8c..2ccbcf610 100644 --- a/src/Node/Node.php +++ b/src/Node/Node.php @@ -47,15 +47,15 @@ class Node implements \Countable, \IteratorAggregate { foreach ($nodes as $name => $node) { if (!$node instanceof self) { - throw new \InvalidArgumentException(\sprintf('Using "%s" for the value of node "%s" of "%s" is not supported. You must pass a \Twig\Node\Node instance.', \is_object($node) ? \get_class($node) : (null === $node ? 'null' : \gettype($node)), $name, static::class)); + throw new \InvalidArgumentException(\sprintf('Using "%s" for the value of node "%s" of "%s" is not supported. You must pass a \Twig\Node\Node instance.', \is_object($node) ? $node::class : (null === $node ? 'null' : \gettype($node)), $name, static::class)); } } $this->nodes = $nodes; $this->attributes = $attributes; $this->lineno = $lineno; - if (func_num_args() > 3) { - trigger_deprecation('twig/twig', '3.12', sprintf('The "tag" constructor argument of the "%s" class is deprecated and ignored (check which TokenParser class set it to "%s"), the tag is now automatically set by the Parser when needed.', static::class, func_get_arg(3) ?: 'null')); + if (\func_num_args() > 3) { + trigger_deprecation('twig/twig', '3.12', \sprintf('The "tag" constructor argument of the "%s" class is deprecated and ignored (check which TokenParser class set it to "%s"), the tag is now automatically set by the Parser when needed.', static::class, func_get_arg(3) ?: 'null')); } } diff --git a/src/Template.php b/src/Template.php index e6bf2486a..e8368c5d9 100644 --- a/src/Template.php +++ b/src/Template.php @@ -382,7 +382,7 @@ abstract class Template } /** - * @return iterable + * @return iterable */ public function yield(array $context, array $blocks = []): iterable { @@ -412,7 +412,7 @@ abstract class Template } /** - * @return iterable + * @return iterable */ public function yieldBlock($name, array $context, array $blocks = [], $useBlocks = true, ?self $templateContext = null): iterable { @@ -472,7 +472,7 @@ abstract class Template * @param array $context The context * @param array $blocks The current set of blocks * - * @return iterable + * @return iterable */ public function yieldParentBlock($name, array $context, array $blocks = []): iterable { @@ -491,7 +491,7 @@ abstract class Template * @param array $context An array of parameters to pass to the template * @param array $blocks An array of blocks to pass to the template * - * @return iterable + * @return iterable */ abstract protected function doDisplay(array $context, array $blocks = []): iterable; } diff --git a/src/Test/NodeTestCase.php b/src/Test/NodeTestCase.php index 09b16ace7..bac0ea6d0 100644 --- a/src/Test/NodeTestCase.php +++ b/src/Test/NodeTestCase.php @@ -118,7 +118,7 @@ abstract class NodeTestCase extends TestCase final public static function checkDataProvider(): void { $r = new \ReflectionMethod(static::class, 'getTests'); - if ($r->getDeclaringClass()->getName() !== self::class) { + if (self::class !== $r->getDeclaringClass()->getName()) { trigger_deprecation('twig/twig', '3.13', 'Implementing "%s::getTests()" in "%s" is deprecated, implement "provideTests()" instead.', self::class, static::class); } } diff --git a/src/TokenParser/TypesTokenParser.php b/src/TokenParser/TypesTokenParser.php index b579fd0c0..2172ed263 100644 --- a/src/TokenParser/TypesTokenParser.php +++ b/src/TokenParser/TypesTokenParser.php @@ -62,14 +62,14 @@ final class TypesTokenParser extends AbstractTokenParser $first = false; $nameToken = $stream->expect(Token::NAME_TYPE); - $isOptional = $stream->nextIf(Token::PUNCTUATION_TYPE, '?') !== null; + $isOptional = null !== $stream->nextIf(Token::PUNCTUATION_TYPE, '?'); $stream->expect(Token::PUNCTUATION_TYPE, ':', 'A type name must be followed by a colon (:)'); $valueToken = $stream->expect(Token::STRING_TYPE); $types[$nameToken->getValue()] = [ - 'type' => $valueToken->getValue(), + 'type' => $valueToken->getValue(), 'optional' => $isOptional, ]; } diff --git a/tests/EnvironmentTest.php b/tests/EnvironmentTest.php index ff8dc741b..5f8d21bb9 100644 --- a/tests/EnvironmentTest.php +++ b/tests/EnvironmentTest.php @@ -178,7 +178,7 @@ class EnvironmentTest extends TestCase // force compilation $twig = new Environment($loader = new ArrayLoader(['index' => '{{ foo }}']), $options); - $twig->addExtension($extension = new class extends AbstractExtension { + $twig->addExtension($extension = new class() extends AbstractExtension { public bool $throw = false; public function getFilters(): array diff --git a/tests/Extension/CoreTest.php b/tests/Extension/CoreTest.php index 2272bb4d0..e869f96c7 100644 --- a/tests/Extension/CoreTest.php +++ b/tests/Extension/CoreTest.php @@ -43,7 +43,7 @@ class CoreTest extends TestCase /** * @dataProvider provideCycleInvalidCases */ - public function testCycleFunctionThrowRuntimeError($values, mixed $position = null) + public function testCycleFunctionThrowRuntimeError($values, mixed $position = null) { $this->expectException(RuntimeError::class); CoreExtension::cycle($values, $position ?? 0); @@ -53,7 +53,8 @@ class CoreTest extends TestCase { return [ 'empty' => [[]], - 'non-countable' => [new class extends \ArrayObject{}], + 'non-countable' => [new class() extends \ArrayObject { + }], ]; } diff --git a/tests/Extension/SandboxTest.php b/tests/Extension/SandboxTest.php index 70052b859..d24a06c67 100644 --- a/tests/Extension/SandboxTest.php +++ b/tests/Extension/SandboxTest.php @@ -76,7 +76,7 @@ class SandboxTest extends TestCase $twig = $this->getEnvironment(true, [], self::$templates, []); $this->expectException(SecurityError::class); - $this->expectExceptionMessageMatches(sprintf('/Tag "%s" is not allowed in "index \(string template .+?\)" at line 1/', $tag)); + $this->expectExceptionMessageMatches(\sprintf('/Tag "%s" is not allowed in "index \(string template .+?\)" at line 1/', $tag)); $twig->createTemplate($template, 'index')->render([]); } @@ -90,7 +90,7 @@ class SandboxTest extends TestCase yield ['do', '{% do 1 + 2 %}']; yield ['embed', '{% embed "base.twig" %}{% endembed %}']; // To be uncommented in 4.0 - //yield ['extends', '{% extends "base.twig" %}']; + // yield ['extends', '{% extends "base.twig" %}']; yield ['flush', '{% flush %}']; yield ['for', '{% for i in 1..2 %}{% endfor %}']; yield ['from', '{% from "macros" import foo %}']; @@ -101,7 +101,7 @@ class SandboxTest extends TestCase yield ['sandbox', '{% sandbox %}{% endsandbox %}']; yield ['set', '{% set foo = 1 %}']; // To be uncommented in 4.0 - //yield ['use', '{% use "1_empty" %}']; + // yield ['use', '{% use "1_empty" %}']; yield ['with', '{% with foo %}{% endwith %}']; } @@ -112,7 +112,7 @@ class SandboxTest extends TestCase */ public function testSandboxForExtendsAndUseTags(string $tag, string $template) { - $this->expectDeprecation(sprintf('Since twig/twig 3.12: The "%s" tag is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed.', $tag)); + $this->expectDeprecation(\sprintf('Since twig/twig 3.12: The "%s" tag is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed.', $tag)); $twig = $this->getEnvironment(true, [], self::$templates, []); $twig->createTemplate($template, 'index')->render([]); diff --git a/tests/Node/Expression/FilterTest.php b/tests/Node/Expression/FilterTest.php index 1339b07dd..78bf50664 100644 --- a/tests/Node/Expression/FilterTest.php +++ b/tests/Node/Expression/FilterTest.php @@ -179,7 +179,7 @@ class FilterTest extends NodeTestCase private static function createExtension(): AbstractExtension { - return new class extends AbstractExtension { + return new class() extends AbstractExtension { public function getFilters(): array { return [ diff --git a/tests/ParserTest.php b/tests/ParserTest.php index 4d61c2341..dc45fd66d 100644 --- a/tests/ParserTest.php +++ b/tests/ParserTest.php @@ -197,7 +197,7 @@ EOF $this->assertNull($argumentNodes->getNode('po')->getAttribute('value')); $this->assertFalse($argumentNodes->getNode('lo')->hasAttribute('is_implicit')); - $this->assertSame(true, $argumentNodes->getNode('lo')->getAttribute('value')); + $this->assertTrue($argumentNodes->getNode('lo')->getAttribute('value')); } protected function getParser() diff --git a/tests/TemplateTest.php b/tests/TemplateTest.php index 801701af2..7018f4afb 100644 --- a/tests/TemplateTest.php +++ b/tests/TemplateTest.php @@ -452,12 +452,12 @@ class TemplateForTest extends Template return $this->name; } - public function getDebugInfo() : array + public function getDebugInfo(): array { return []; } - public function getSourceContext() : Source + public function getSourceContext(): Source { return new Source('', $this->getTemplateName()); } diff --git a/tests/TokenParser/TypesTokenParserTest.php b/tests/TokenParser/TypesTokenParserTest.php index 57ccb77cb..85ce6f7a5 100644 --- a/tests/TokenParser/TypesTokenParserTest.php +++ b/tests/TokenParser/TypesTokenParserTest.php @@ -35,7 +35,7 @@ class TypesTokenParserTest extends TestCase [ '{% types {foo: "bar"} %}', [ - 'foo' => ['type' => 'bar', 'optional' => false] + 'foo' => ['type' => 'bar', 'optional' => false], ], ], @@ -43,7 +43,7 @@ class TypesTokenParserTest extends TestCase [ '{% types {foo: "bar",} %}', [ - 'foo' => ['type' => 'bar', 'optional' => false] + 'foo' => ['type' => 'bar', 'optional' => false], ], ], @@ -51,7 +51,7 @@ class TypesTokenParserTest extends TestCase [ '{% types {foo?: "bar"} %}', [ - 'foo' => ['type' => 'bar', 'optional' => true] + 'foo' => ['type' => 'bar', 'optional' => true], ], ], @@ -61,7 +61,7 @@ class TypesTokenParserTest extends TestCase [ 'foo' => ['type' => 'foo', 'optional' => false], 'bar' => ['type' => 'foo', 'optional' => true], - 'baz' => ['type' => 'baz', 'optional' => false] + 'baz' => ['type' => 'baz', 'optional' => false], ], ], ]; From 978f749b43ccfe2ed0b7d9c160a534907fe9ca3e Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 6 Sep 2024 12:58:38 +0200 Subject: [PATCH 05/10] Fix minor things --- src/Node/MacroNode.php | 2 +- src/Util/ReflectionCallable.php | 2 +- tests/TokenParser/TypesTokenParserTest.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Node/MacroNode.php b/src/Node/MacroNode.php index d54b8ac72..f3e9d7a10 100644 --- a/src/Node/MacroNode.php +++ b/src/Node/MacroNode.php @@ -86,7 +86,7 @@ class MacroNode extends Node ; } - $node = new CaptureNode($this->getNode('body'), $this->getNode('body')->lineno, $this->getNode('body')->tag); + $node = new CaptureNode($this->getNode('body'), $this->getNode('body')->lineno); $compiler ->write('') diff --git a/src/Util/ReflectionCallable.php b/src/Util/ReflectionCallable.php index 9b183f14d..16734d9df 100644 --- a/src/Util/ReflectionCallable.php +++ b/src/Util/ReflectionCallable.php @@ -25,7 +25,7 @@ final class ReflectionCallable private $name; public function __construct( - private TwigCallableInterface $twigCallable, + TwigCallableInterface $twigCallable, ) { $callable = $twigCallable->getCallable(); if (\is_string($callable) && false !== $pos = strpos($callable, '::')) { diff --git a/tests/TokenParser/TypesTokenParserTest.php b/tests/TokenParser/TypesTokenParserTest.php index 85ce6f7a5..400820fca 100644 --- a/tests/TokenParser/TypesTokenParserTest.php +++ b/tests/TokenParser/TypesTokenParserTest.php @@ -14,7 +14,7 @@ class TypesTokenParserTest extends TestCase public function testMappingParsing(string $template, array $expected): void { $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); - $stream = $env->tokenize($source = new Source($template, '')); + $stream = $env->tokenize(new Source($template, '')); $parser = new Parser($env); $typesNode = $parser->parse($stream)->getNode('body')->getNode('0'); From a219d9e77f318d499de4bd4ce145667949c579b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edi=20Modri=C4=87?= Date: Fri, 6 Sep 2024 12:28:44 +0200 Subject: [PATCH 06/10] Fix wrong format of `Environment::VERSION_ID` constant --- src/Environment.php | 2 +- tests/EnvironmentTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Environment.php b/src/Environment.php index dbdb4e7d9..5b076a781 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -44,7 +44,7 @@ use Twig\TokenParser\TokenParserInterface; class Environment { public const VERSION = '3.13.0-DEV'; - public const VERSION_ID = 301300; + public const VERSION_ID = 31300; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 13; public const RELEASE_VERSION = 0; diff --git a/tests/EnvironmentTest.php b/tests/EnvironmentTest.php index ff8dc741b..7ae31155f 100644 --- a/tests/EnvironmentTest.php +++ b/tests/EnvironmentTest.php @@ -50,7 +50,7 @@ class EnvironmentTest extends TestCase $this->assertEquals(Environment::MINOR_VERSION, $exploded[1]); $this->assertEquals(Environment::RELEASE_VERSION, $exploded[2]); - $this->assertEquals(Environment::VERSION_ID, \sprintf('%s0%s0%s', $exploded[0], $exploded[1], $exploded[2])); + $this->assertEquals(Environment::VERSION_ID, Environment::MAJOR_VERSION * 10000 + Environment::MINOR_VERSION * 100 + Environment::RELEASE_VERSION); } public function testAutoescapeOption() From afc0eb63dc66c248c5a94504dc2b255bc9b86575 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 7 Sep 2024 10:01:12 +0200 Subject: [PATCH 07/10] Prepare the 3.13.0 release --- CHANGELOG | 2 +- src/Environment.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 0b3cca744..88c1552cc 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -# 3.13.0 (2024-XX-XX) +# 3.13.0 (2024-09-07) * Add the `types` tag (experimental) * Deprecate the `Twig\Test\NodeTestCase::getTests()` data provider, override `provideTests()` instead. diff --git a/src/Environment.php b/src/Environment.php index 5b076a781..b8c2989e6 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -43,12 +43,12 @@ use Twig\TokenParser\TokenParserInterface; */ class Environment { - public const VERSION = '3.13.0-DEV'; + public const VERSION = '3.13.0'; public const VERSION_ID = 31300; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 13; public const RELEASE_VERSION = 0; - public const EXTRA_VERSION = 'DEV'; + public const EXTRA_VERSION = ''; private $charset; private $loader; From e1b64b1937d8e1f7ca4cd0cf9e62ff48fa240b07 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 7 Sep 2024 12:53:18 +0200 Subject: [PATCH 08/10] Bump version --- CHANGELOG | 4 ++++ src/Environment.php | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 88c1552cc..70dc4acd8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +# 3.13.1 (2024-XX-XX) + + * n/a + # 3.13.0 (2024-09-07) * Add the `types` tag (experimental) diff --git a/src/Environment.php b/src/Environment.php index b8c2989e6..1456d3744 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -43,12 +43,12 @@ use Twig\TokenParser\TokenParserInterface; */ class Environment { - public const VERSION = '3.13.0'; - public const VERSION_ID = 31300; + public const VERSION = '3.13.1-DEV'; + public const VERSION_ID = 31301; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 13; - public const RELEASE_VERSION = 0; - public const EXTRA_VERSION = ''; + public const RELEASE_VERSION = 1; + public const EXTRA_VERSION = 'DEV'; private $charset; private $loader; From b86575cfd6ad0c1872a2c16aefaeb6a0c8ef8174 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 7 Sep 2024 14:00:41 +0200 Subject: [PATCH 09/10] Deprecate Environment::mergeGlobals() --- CHANGELOG | 2 +- doc/deprecated.rst | 14 ++++++++++++++ src/Environment.php | 13 +++++-------- src/Node/MacroNode.php | 4 ++-- src/Node/WithNode.php | 2 +- src/Template.php | 2 +- src/TemplateWrapper.php | 4 ++-- tests/Node/MacroTest.php | 8 ++++---- 8 files changed, 30 insertions(+), 19 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 70dc4acd8..c0f26c20d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,6 @@ # 3.13.1 (2024-XX-XX) - * n/a + * Deprecate `Environment::mergeGlobals()` # 3.13.0 (2024-09-07) diff --git a/doc/deprecated.rst b/doc/deprecated.rst index e28ecc62b..0490c2ded 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -223,3 +223,17 @@ Testing Utilities * The data providers ``getTests()`` and ``getLegacyTests()`` on ``Twig\Test\IntegrationTestCase`` are considered final als of Twig 3.13. + +Environment +----------- + +* The ``Twig\Environment::mergeGlobals()`` method is deprecated as of Twig 3.13 + and will be removed in Twig 4.0: + + Before:: + + $context = $twig->mergeGlobals($context); + + After:: + + $context += $twig->getGlobals(); diff --git a/src/Environment.php b/src/Environment.php index 1456d3744..297efa9cc 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -830,17 +830,14 @@ class Environment return array_merge($this->extensionSet->getGlobals(), $this->globals); } + /** + * @deprecated since Twig 3.13 + */ public function mergeGlobals(array $context): array { - // we don't use array_merge as the context being generally - // bigger than globals, this code is faster. - foreach ($this->getGlobals() as $key => $value) { - if (!\array_key_exists($key, $context)) { - $context[$key] = $value; - } - } + trigger_deprecation('twig/twig', '3.13', 'The "%s" method is deprecated.', __METHOD__); - return $context; + return $context + $this->getGlobals(); } /** diff --git a/src/Node/MacroNode.php b/src/Node/MacroNode.php index f3e9d7a10..5a2543a9f 100644 --- a/src/Node/MacroNode.php +++ b/src/Node/MacroNode.php @@ -73,7 +73,7 @@ class MacroNode extends Node ->write("{\n") ->indent() ->write("\$macros = \$this->macros;\n") - ->write("\$context = \$this->env->mergeGlobals([\n") + ->write("\$context = [\n") ->indent() ; @@ -94,7 +94,7 @@ class MacroNode extends Node ->raw(' => ') ->raw("\$__varargs__,\n") ->outdent() - ->write("]);\n\n") + ->write("] + \$this->env->getGlobals();\n\n") ->write("\$blocks = [];\n\n") ->write('return ') ->subcompile($node) diff --git a/src/Node/WithNode.php b/src/Node/WithNode.php index f9104948b..487e2800b 100644 --- a/src/Node/WithNode.php +++ b/src/Node/WithNode.php @@ -61,7 +61,7 @@ class WithNode extends Node $compiler->write("\$context = [];\n"); } - $compiler->write(\sprintf("\$context = \$this->env->mergeGlobals(array_merge(\$context, \$%s));\n", $varsName)); + $compiler->write(\sprintf("\$context = \$%s + \$context + \$this->env->getGlobals();\n", $varsName)); } $compiler diff --git a/src/Template.php b/src/Template.php index e8368c5d9..7b3ce8161 100644 --- a/src/Template.php +++ b/src/Template.php @@ -386,7 +386,7 @@ abstract class Template */ public function yield(array $context, array $blocks = []): iterable { - $context = $this->env->mergeGlobals($context); + $context += $this->env->getGlobals(); $blocks = array_merge($this->blocks, $blocks); try { diff --git a/src/TemplateWrapper.php b/src/TemplateWrapper.php index c31f50161..135c59188 100644 --- a/src/TemplateWrapper.php +++ b/src/TemplateWrapper.php @@ -57,12 +57,12 @@ final class TemplateWrapper public function renderBlock(string $name, array $context = []): string { - return $this->template->renderBlock($name, $this->env->mergeGlobals($context)); + return $this->template->renderBlock($name, $context + $this->env->getGlobals()); } public function displayBlock(string $name, array $context = []) { - $context = $this->env->mergeGlobals($context); + $context += $this->env->getGlobals(); foreach ($this->template->yieldBlock($name, $context) as $data) { echo $data; } diff --git a/tests/Node/MacroTest.php b/tests/Node/MacroTest.php index f738c275f..6fb6062cb 100644 --- a/tests/Node/MacroTest.php +++ b/tests/Node/MacroTest.php @@ -49,11 +49,11 @@ class MacroTest extends NodeTestCase public function macro_foo(\$__foo__ = null, \$__bar__ = "Foo", ...\$__varargs__) { \$macros = \$this->macros; - \$context = \$this->env->mergeGlobals([ + \$context = [ "foo" => \$__foo__, "bar" => \$__bar__, "varargs" => \$__varargs__, - ]); + ] + \$this->env->getGlobals(); \$blocks = []; @@ -71,11 +71,11 @@ EOF public function macro_foo(\$__foo__ = null, \$__bar__ = "Foo", ...\$__varargs__) { \$macros = \$this->macros; - \$context = \$this->env->mergeGlobals([ + \$context = [ "foo" => \$__foo__, "bar" => \$__bar__, "varargs" => \$__varargs__, - ]); + ] + \$this->env->getGlobals(); \$blocks = []; From 57f6119d31a7fad57ef233edd609864e574eee6a Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 7 Sep 2024 14:36:55 +0200 Subject: [PATCH 10/10] Remove obsolete code --- src/ExtensionSet.php | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/ExtensionSet.php b/src/ExtensionSet.php index 32377b0fc..d2848b59c 100644 --- a/src/ExtensionSet.php +++ b/src/ExtensionSet.php @@ -329,12 +329,7 @@ final class ExtensionSet continue; } - $extGlobals = $extension->getGlobals(); - if (!\is_array($extGlobals)) { - throw new \UnexpectedValueException(\sprintf('"%s::getGlobals()" must return an array of globals.', \get_class($extension))); - } - - $globals = array_merge($globals, $extGlobals); + $globals = array_merge($globals, $extension->getGlobals()); } if ($this->initialized) {