Skip to content

Commit 93f58d3

Browse files
committed
Fix calling partials named true, false, null, or undefined
{{> true}}, {{> false}}, {{> null}}, and {{> undefined}} fell through to compileExpression, which produced PHP literal values instead of string names. Boolean literals caused a type error; null/undefined silently rendered nothing. Also added regression tests for helpers and context fields with literal names.
1 parent 3ded108 commit 93f58d3

2 files changed

Lines changed: 65 additions & 27 deletions

File tree

src/Compiler.php

Lines changed: 15 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -410,12 +410,10 @@ private function PartialStatement(PartialStatement $statement): string
410410
if ($name instanceof SubExpression) {
411411
$p = $this->SubExpression($name);
412412
$this->context->usedDynPartial++;
413-
} elseif ($name instanceof PathExpression || $name instanceof StringLiteral || $name instanceof NumberLiteral) {
413+
} else {
414414
$partialName = $this->resolvePartialName($name);
415415
$p = self::quote($partialName);
416416
$this->resolveAndCompilePartial($partialName);
417-
} else {
418-
$p = $this->compileExpression($name);
419417
}
420418

421419
$vars = $this->compilePartialParams($statement->params, $statement->hash);
@@ -452,36 +450,29 @@ private function PartialBlockStatement(PartialBlockStatement $statement): string
452450
$name = $statement->name;
453451
$depsBefore = count($this->programDepStack[array_key_last($this->programDepStack)]);
454452
$body = $this->compileProgram($statement->program);
455-
$partialName = null;
456-
$found = false;
457453

458-
if ($name instanceof PathExpression || $name instanceof StringLiteral || $name instanceof NumberLiteral) {
459-
$partialName = $this->resolvePartialName($name);
460-
$p = self::quote($partialName);
461-
$found = ($this->context->usedPartial[$partialName] ?? '') !== '';
462-
463-
if (!$found && !str_starts_with($partialName, '@partial-block')) {
464-
$cnt = $this->resolvePartial($partialName);
465-
if ($cnt !== null) {
466-
$this->context->usedPartial[$partialName] = $cnt;
467-
$this->compilePartialTemplate($partialName, $cnt);
468-
$found = true;
469-
}
470-
}
454+
$partialName = $this->resolvePartialName($name);
455+
$p = self::quote($partialName);
456+
$found = ($this->context->usedPartial[$partialName] ?? '') !== '';
471457

472-
// Mark as known for runtime resolution; not added to partialCode so $blockParams scope is preserved.
473-
$this->context->usedPartial[$partialName] ??= '';
474-
} else {
475-
$p = $this->compileExpression($name);
458+
if (!$found && !str_starts_with($partialName, '@partial-block')) {
459+
$cnt = $this->resolvePartial($partialName);
460+
if ($cnt !== null) {
461+
$this->context->usedPartial[$partialName] = $cnt;
462+
$this->compilePartialTemplate($partialName, $cnt);
463+
$found = true;
464+
}
476465
}
477466

467+
// Mark as known for runtime resolution; not added to partialCode so $blockParams scope is preserved.
468+
$this->context->usedPartial[$partialName] ??= '';
478469
$vars = $this->compilePartialParams($statement->params, $statement->hash);
479470

480471
// Capture $blockParams and any hoisted program vars so the partial block body can access them.
481472
$useVars = $this->buildInlineUseClause($depsBefore);
482473
$bodyClosure = self::templateClosure($body, useVars: $useVars);
483474

484-
if ($partialName !== null && !$found) {
475+
if (!$found) {
485476
// Register the block body as a fallback partial only if no runtime partial with this name exists yet.
486477
$parts[] = "(isset(\$cx->inlinePartials[$p]) || isset(\$cx->partials[$p]) ? '' : "
487478
. self::getRuntimeFunc('setInlinePartial', "\$cx, $p, $bodyClosure") . ')';
@@ -798,7 +789,7 @@ private function buildBasePath(bool $data, int $depth): string
798789
/**
799790
* Resolve the name of a non-SubExpression partial reference.
800791
*/
801-
private function resolvePartialName(PathExpression|StringLiteral|NumberLiteral $name): string
792+
private function resolvePartialName(PathExpression|Literal $name): string
802793
{
803794
return $name instanceof PathExpression ? $name->original : $this->getLiteralKeyName($name);
804795
}

tests/RegressionTest.php

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace DevTheorem\Handlebars\Test;
44

5+
use Closure;
56
use DevTheorem\Handlebars\Handlebars;
67
use DevTheorem\Handlebars\HelperOptions;
78
use DevTheorem\Handlebars\Options;
@@ -12,7 +13,7 @@
1213
/**
1314
* @phpstan-type RegIssue array{
1415
* template: string, expected: string, data?: mixed, options?: Options,
15-
* helpers?: array<string, \Closure>, partials?: array<string, string>,
16+
* helpers?: array<Closure>, partials?: array<string>,
1617
* vars?: array<mixed>,
1718
* }
1819
*/
@@ -44,8 +45,8 @@ public function testLog(): void
4445
}
4546

4647
/**
47-
* @param array<string, \Closure> $helpers
48-
* @param array<string, string> $partials
48+
* @param array<Closure> $helpers
49+
* @param array<string> $partials
4950
* @param array<mixed> $vars
5051
*/
5152
#[DataProvider("helperProvider")]
@@ -1154,6 +1155,18 @@ public static function partialProvider(): array
11541155
'data' => ['str' => 'hello'],
11551156
'expected' => 'val',
11561157
],
1158+
1159+
'partials can have literal names' => [
1160+
'template' => '{{> 42}} {{> true}} {{> false}} {{> null}} {{> undefined}}',
1161+
'partials' => [
1162+
'42' => 'p42',
1163+
'true' => 'pTrue',
1164+
'false' => 'pFalse',
1165+
'null' => 'pNull',
1166+
'undefined' => 'pUndefined',
1167+
],
1168+
'expected' => 'p42 pTrue pFalse pNull pUndefined',
1169+
],
11571170
];
11581171
}
11591172

@@ -2510,6 +2523,40 @@ public static function literalPathProvider(): array
25102523
'expected' => 'helper',
25112524
'options' => new Options(assumeObjects: true),
25122525
],
2526+
2527+
'helpers can have literal names' => [
2528+
'template' => '{{42}} {{true}} {{false}} {{null}} {{undefined}}',
2529+
'helpers' => [
2530+
'42' => fn() => 'h42',
2531+
'true' => fn() => 'hTrue',
2532+
'false' => fn() => 'hFalse',
2533+
'null' => fn() => 'hNull',
2534+
'undefined' => fn() => 'hUndefined',
2535+
],
2536+
'expected' => 'h42 hTrue hFalse hNull hUndefined',
2537+
],
2538+
'closures in data can have literal names' => [
2539+
'template' => '{{42}} {{true}} {{false}} {{null}} {{undefined}}',
2540+
'data' => [
2541+
'42' => fn() => 'c42',
2542+
'true' => fn() => 'cTrue',
2543+
'false' => fn() => 'cFalse',
2544+
'null' => fn() => 'cNull',
2545+
'undefined' => fn() => 'cUndefined',
2546+
],
2547+
'expected' => 'c42 cTrue cFalse cNull cUndefined',
2548+
],
2549+
'context fields can have literal names' => [
2550+
'template' => '{{42}} {{true}} {{false}} {{null}} {{undefined}}',
2551+
'data' => [
2552+
'42' => 'd42',
2553+
'true' => 'dTrue',
2554+
'false' => 'dFalse',
2555+
'null' => 'dNull',
2556+
'undefined' => 'dUndefined',
2557+
],
2558+
'expected' => 'd42 dTrue dFalse dNull dUndefined',
2559+
],
25132560
];
25142561
}
25152562

0 commit comments

Comments
 (0)