Repository: twigphp/Twig (php)

Issue:
Uninitialized string offset warning in Lexer
With Twig 3.9.3, a template containing only `{{=` triggers a warning in the lexer:
```
PHP Warning:  Uninitialized string offset 3 in /home/pierre/www/fuzzer/vendor/twig/twig/src/Lexer.php on line 324
```

This code reproduces the warning:
```php
<?php

require __DIR__.'/vendor/autoload.php';

$loader = new \Twig\Loader\ArrayLoader(['test.html' => '{{=']);
$twig = new \Twig\Environment($loader);

echo $twig->render('test.html');
```

It then also fails with a SyntaxError (which is expected):
```
PHP Fatal error:  Uncaught Twig\Error\SyntaxError: Unexpected token "operator" of value "=" in "test.html" at line 1. in /home/pierre/www/fuzzer/vendor/twig/twig/src/ExpressionParser.php:287
```

But when using the Symfony ErrorHandler, it will stop at the warning and display it to the dev, which can be confusing.

Pull request description:
Fix a warning
Closes #4048 


Patch:
diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php
index c7c6d14f..dbc5f614 100644
--- a/src/Extension/CoreExtension.php
+++ b/src/Extension/CoreExtension.php
@@ -1837,7 +1837,7 @@ final class CoreExtension extends AbstractExtension
      */
     public static function checkArrowInSandbox(Environment $env, $arrow, $thing, $type)
     {
-        if (!$arrow instanceof \Closure && $env->hasExtension(SandboxExtension::class) && $env->getExtension(SandboxExtension::class)->isSandboxed()) {
+        if (!$arrow instanceof \Closure && $env->hasExtension('\Twig\Extension\SandboxExtension') && $env->getExtension('\Twig\Extension\SandboxExtension')->isSandboxed()) {
             throw new RuntimeError(sprintf('The callable passed to the "%s" %s must be a Closure in sandbox mode.', $thing, $type));
         }
     }
diff --git a/src/Lexer.php b/src/Lexer.php
index 4d7cffdd..e15e896f 100644
--- a/src/Lexer.php
+++ b/src/Lexer.php
@@ -321,7 +321,7 @@ class Lexer
             $this->moveCursor('...');
         }
         // arrow function
-        elseif ('=' === $this->code[$this->cursor] && '>' === $this->code[$this->cursor + 1]) {
+        elseif ('=' === $this->code[$this->cursor] && ($this->cursor + 1 < $this->end) && '>' === $this->code[$this->cursor + 1]) {
             $this->pushToken(Token::ARROW_TYPE, '=>');
             $this->moveCursor('=>');
         }


Test patch:
diff --git a/tests/EnvironmentTest.php b/tests/EnvironmentTest.php
index e7eb26e4..688024b8 100644
--- a/tests/EnvironmentTest.php
+++ b/tests/EnvironmentTest.php
@@ -271,8 +271,8 @@ class EnvironmentTest extends TestCase
     {
         $twig = new Environment($this->createMock(LoaderInterface::class));
         $twig->addExtension($ext = new EnvironmentTest_Extension());
-        $this->assertSame($ext, $twig->getExtension(EnvironmentTest_Extension::class));
-        $this->assertSame($ext, $twig->getExtension(EnvironmentTest_Extension::class));
+        $this->assertSame($ext, $twig->getExtension('Twig\Tests\EnvironmentTest_Extension'));
+        $this->assertSame($ext, $twig->getExtension('\Twig\Tests\EnvironmentTest_Extension'));
     }
 
     public function testAddExtension()
diff --git a/tests/Extension/EscaperTest.php b/tests/Extension/EscaperTest.php
index c4657891..554a7c8f 100644
--- a/tests/Extension/EscaperTest.php
+++ b/tests/Extension/EscaperTest.php
@@ -400,7 +400,7 @@ class Twig_Tests_Extension_EscaperTest extends TestCase
     {
         $obj = new Extension_TestClass();
         $twig = new Environment($this->createMock(LoaderInterface::class));
-        $twig->getExtension(EscaperExtension::class)->setSafeClasses($safeClasses);
+        $twig->getExtension('\Twig\Extension\EscaperExtension')->setSafeClasses($safeClasses);
         $this->assertSame($escapedHtml, EscaperExtension::escape($twig, $obj, 'html', null, true));
         $this->assertSame($escapedJs, EscaperExtension::escape($twig, $obj, 'js', null, true));
     }
diff --git a/tests/LexerTest.php b/tests/LexerTest.php
index ad62c22a..2aad47ac 100644
--- a/tests/LexerTest.php
+++ b/tests/LexerTest.php
@@ -378,4 +378,27 @@ bar
         // can be executed without throwing any exceptions
         $this->addToAssertionCount(1);
     }
+
+    /**
+     * @dataProvider getTemplateForErrorsAtTheEndOfTheStream
+     */
+    public function testErrorsAtTheEndOfTheStream(string $template)
+    {
+        $lexer = new Lexer(new Environment($this->createMock(LoaderInterface::class)));
+        set_error_handler(function () {
+            $this->fail('Lexer should not emit warnings.');
+        });
+        try {
+            $lexer->tokenize(new Source($template, 'index'));
+            $this->addToAssertionCount(1);
+        } finally {
+            restore_error_handler();
+        }
+    }
+
+    public function getTemplateForErrorsAtTheEndOfTheStream()
+    {
+        yield ['{{ ='];
+        yield ['{{ ..'];
+    }
 }