diff --git a/src/Parser/Parser.php b/src/Parser/Parser.php index 188a912..62de238 100644 --- a/src/Parser/Parser.php +++ b/src/Parser/Parser.php @@ -63,6 +63,7 @@ public function parse(string $code, string $templatePath = 'eval code'): array $state = $parserStatus->getState(); $scannerStatus = ScannerStatus::OK; + $prevToken = 0; while (($scannerStatus = $scanner->scanForToken()) === ScannerStatus::OK) { $token = $scanner->getToken(); @@ -131,7 +132,8 @@ public function parse(string $code, string $templatePath = 'eval code'): array $parser, $parserStatus, $token, - $state + $state, + $prevToken ), CompilerOpcode::ENDSWITCH->value => $this->handleEndswitch($parser, $parserStatus, $state), CompilerOpcode::RAW_FRAGMENT->value => $this->handleRawFragment( @@ -181,6 +183,11 @@ public function parse(string $code, string $templatePath = 'eval code'): array break; } + // whitespace inside delimiters arrives as IGNORE; skip it + if ($opcode !== CompilerOpcode::IGNORE->value) { + $prevToken = $opcode; + } + $state->setEnd($state->getStart()); } @@ -285,13 +292,22 @@ private function handleCase(phvolt_Parser $parser, Status $parserStatus): void $parser->phvolt_(Opcode::CASE->value); } + /** + * "default" is the {% default %} clause only when it is inside a switch + * and directly follows the opening delimiter; anywhere else (e.g. the + * |default() filter) it is a plain identifier. + */ private function handleDefault( phvolt_Parser $parser, Status $parserStatus, Token $token, - State $state + State $state, + int $prevToken ): void { - if ($state->getSwitchLevel() !== 0) { + if ( + $state->getSwitchLevel() !== 0 && + $prevToken === CompilerOpcode::OPEN_DELIMITER->value + ) { $parser->phvolt_(Opcode::DEFAULT->value); return; diff --git a/tests/unit/Compiler/CompileSwitchTest.php b/tests/unit/Compiler/CompileSwitchTest.php new file mode 100644 index 0000000..79c7e26 --- /dev/null +++ b/tests/unit/Compiler/CompileSwitchTest.php @@ -0,0 +1,165 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Tests\Unit\Compiler; + +use Phalcon\Volt\Compiler; +use PHPUnit\Framework\TestCase; + +final class CompileSwitchTest extends TestCase +{ + private Compiler $compiler; + + public function setUp(): void + { + $this->compiler = new Compiler(); + } + + /** + * Tests the "default" filter inside a case block of a switch + * + * @return void + * + * @author Phalcon Team + * @since 2026-06-10 + * + * @issue https://github.com/phalcon/cphalcon/issues/16003 + */ + public function testMvcViewEngineVoltCompilerCompileSwitchCaseWithDefaultFilter(): void + { + $actual = $this->compiler->compileString( + "{% set aNumber = 1 %} +{% switch aNumber %} + {% case 0 %} + {{ greatText }} + {% break %} + {% case 1 %} + {{ false|default('simple text') }} + {% break %} +{% endswitch %}" + ); + + $this->assertStringContainsString('switch ($aNumber):', $actual); + $this->assertStringContainsString('case 0:', $actual); + $this->assertStringContainsString('case 1:', $actual); + $this->assertStringContainsString( + "(empty(false) ? ('simple text') : (false))", + $actual + ); + } + + /** + * Tests the {% default %} clause surrounded by extra whitespace + * + * @return void + * + * @author Phalcon Team + * @since 2026-06-10 + * + * @issue https://github.com/phalcon/cphalcon/issues/16003 + */ + public function testMvcViewEngineVoltCompilerCompileSwitchDefaultClauseWhitespace(): void + { + $actual = $this->compiler->compileString( + "{% switch x %}{% case 1 %}one{% default %}other{% endswitch %}" + ); + + $this->assertStringContainsString('default:', $actual); + } + + /** + * Tests the {% default %} clause with whitespace control markers + * + * @return void + * + * @author Phalcon Team + * @since 2026-06-10 + * + * @issue https://github.com/phalcon/cphalcon/issues/16003 + */ + public function testMvcViewEngineVoltCompilerCompileSwitchDefaultClauseWhitespaceControl(): void + { + $actual = $this->compiler->compileString( + "{% switch x %}{% case 1 %}one{%- default -%}other{% endswitch %}" + ); + + $this->assertStringContainsString('default:', $actual); + } + + /** + * Tests the "default" filter inside the {% default %} clause itself + * + * @return void + * + * @author Phalcon Team + * @since 2026-06-10 + * + * @issue https://github.com/phalcon/cphalcon/issues/16003 + */ + public function testMvcViewEngineVoltCompilerCompileSwitchDefaultClauseWithDefaultFilter(): void + { + $actual = $this->compiler->compileString( + "{% switch x %}{% case 1 %}one{% default %}" + . "{{ value|default('unknown') }}{% endswitch %}" + ); + + $this->assertStringContainsString('default:', $actual); + $this->assertStringContainsString( + "(empty(\$value) ? ('unknown') : (\$value))", + $actual + ); + } + + /** + * Tests the "default" filter outside a switch (issue #13242 regression) + * + * @return void + * + * @author Phalcon Team + * @since 2026-06-10 + * + * @issue https://github.com/phalcon/cphalcon/issues/16003 + */ + public function testMvcViewEngineVoltCompilerCompileSwitchDefaultFilterOutsideSwitch(): void + { + $actual = $this->compiler->compileString( + "{{ value|default('unknown') }}" + ); + + $this->assertStringContainsString( + "(empty(\$value) ? ('unknown') : (\$value))", + $actual + ); + } + + /** + * Tests "default" used as a plain identifier inside a switch + * + * @return void + * + * @author Phalcon Team + * @since 2026-06-10 + * + * @issue https://github.com/phalcon/cphalcon/issues/16003 + */ + public function testMvcViewEngineVoltCompilerCompileSwitchDefaultIdentifierInsideSwitch(): void + { + $actual = $this->compiler->compileString( + "{% switch x %}{% case 1 %}" + . "{% set default = 'abc' %}{{ default }}{% endswitch %}" + ); + + $this->assertStringContainsString("\$default = 'abc'", $actual); + $this->assertStringContainsString('', $actual); + } +} diff --git a/tests/unit/Compiler/SwitchTest.php b/tests/unit/Compiler/SwitchTest.php index f283a9b..ba66399 100644 --- a/tests/unit/Compiler/SwitchTest.php +++ b/tests/unit/Compiler/SwitchTest.php @@ -98,6 +98,170 @@ public function testMvcViewEngineVoltParserSwitchCase(): void $this->assertSame($expected, $actual); } + /** + * Tests the "default" filter inside a case block of a switch + * + * @return void + * + * @author Phalcon Team + * @since 2026-06-10 + * + * @issue https://github.com/phalcon/cphalcon/issues/16003 + */ + public function testMvcViewEngineVoltParserSwitchCaseDefaultFilter(): void + { + $source = "{% switch x %}{% case 1 %}" + . "{{ false|default('simple text') }}{% break %}{% endswitch %}"; + $expected = [ + [ + 'type' => 411, + 'expr' => [ + 'type' => 265, + 'value' => 'x', + 'file' => 'eval code', + 'line' => 1, + ], + 'case_clauses' => [ + [ + 'type' => 412, + 'expr' => [ + 'type' => 258, + 'value' => '1', + 'file' => 'eval code', + 'line' => 1, + ], + 'file' => 'eval code', + 'line' => 1, + ], + [ + 'type' => 359, + 'expr' => [ + 'type' => 124, + 'left' => [ + 'type' => 262, + 'file' => 'eval code', + 'line' => 1, + ], + 'right' => [ + 'type' => 350, + 'name' => [ + 'type' => 265, + 'value' => 'default', + 'file' => 'eval code', + 'line' => 1, + ], + 'arguments' => [ + [ + 'expr' => [ + 'type' => 260, + 'value' => 'simple text', + 'file' => 'eval code', + 'line' => 1, + ], + 'file' => 'eval code', + 'line' => 1, + ], + ], + 'file' => 'eval code', + 'line' => 1, + ], + 'file' => 'eval code', + 'line' => 1, + ], + 'file' => 'eval code', + 'line' => 1, + ], + [ + 'type' => 320, + 'file' => 'eval code', + 'line' => 1, + ], + ], + 'file' => 'eval code', + 'line' => 1, + ], + ]; + $actual = $this->compiler->parse($source); + $this->assertSame($expected, $actual); + } + + /** + * Tests the "default" filter inside the {% default %} clause itself + * + * @return void + * + * @author Phalcon Team + * @since 2026-06-10 + * + * @issue https://github.com/phalcon/cphalcon/issues/16003 + */ + public function testMvcViewEngineVoltParserSwitchDefaultClauseDefaultFilter(): void + { + $source = "{% switch x %}{% default %}" + . "{{ value|default('unknown') }}{% endswitch %}"; + $expected = [ + [ + 'type' => 411, + 'expr' => [ + 'type' => 265, + 'value' => 'x', + 'file' => 'eval code', + 'line' => 1, + ], + 'case_clauses' => [ + [ + 'type' => 413, + 'file' => 'eval code', + 'line' => 1, + ], + [ + 'type' => 359, + 'expr' => [ + 'type' => 124, + 'left' => [ + 'type' => 265, + 'value' => 'value', + 'file' => 'eval code', + 'line' => 1, + ], + 'right' => [ + 'type' => 350, + 'name' => [ + 'type' => 265, + 'value' => 'default', + 'file' => 'eval code', + 'line' => 1, + ], + 'arguments' => [ + [ + 'expr' => [ + 'type' => 260, + 'value' => 'unknown', + 'file' => 'eval code', + 'line' => 1, + ], + 'file' => 'eval code', + 'line' => 1, + ], + ], + 'file' => 'eval code', + 'line' => 1, + ], + 'file' => 'eval code', + 'line' => 1, + ], + 'file' => 'eval code', + 'line' => 1, + ], + ], + 'file' => 'eval code', + 'line' => 1, + ], + ]; + $actual = $this->compiler->parse($source); + $this->assertSame($expected, $actual); + } + /** * @return void *