Skip to content

Commit 2ccdd93

Browse files
committed
Simplify compiler code and error tests
1 parent 8b2348f commit 2ccdd93

2 files changed

Lines changed: 51 additions & 94 deletions

File tree

src/Compiler.php

Lines changed: 31 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,8 @@ private function accept(Node $node): string
124124
$node instanceof PartialBlockStatement => $this->PartialBlockStatement($node),
125125
$node instanceof Decorator => $this->Decorator($node),
126126
$node instanceof MustacheStatement => $this->MustacheStatement($node),
127-
$node instanceof ContentStatement => $this->ContentStatement($node),
128-
$node instanceof CommentStatement => $this->CommentStatement($node),
127+
$node instanceof ContentStatement => self::quote($node->value),
128+
$node instanceof CommentStatement => '',
129129
$node instanceof SubExpression => $this->SubExpression($node),
130130
default => throw new \Exception('Unknown type: ' . (new \ReflectionClass($node))->getShortName()),
131131
};
@@ -136,11 +136,10 @@ private function compileExpression(Expression $expr): string
136136
return match (true) {
137137
$expr instanceof SubExpression => $this->SubExpression($expr),
138138
$expr instanceof PathExpression => $this->PathExpression($expr),
139-
$expr instanceof StringLiteral => $this->StringLiteral($expr),
140-
$expr instanceof NumberLiteral => $this->NumberLiteral($expr),
141-
$expr instanceof BooleanLiteral => $this->BooleanLiteral($expr),
142-
$expr instanceof NullLiteral => $this->NullLiteral($expr),
143-
$expr instanceof UndefinedLiteral => $this->UndefinedLiteral($expr),
139+
$expr instanceof StringLiteral => self::quote($expr->value),
140+
$expr instanceof NumberLiteral => (string) $expr->value,
141+
$expr instanceof BooleanLiteral => $expr->value ? 'true' : 'false',
142+
$expr instanceof NullLiteral, $expr instanceof UndefinedLiteral => 'null',
144143
default => throw new \Exception('Unknown expression type: ' . (new \ReflectionClass($expr))->getShortName()),
145144
};
146145
}
@@ -209,7 +208,7 @@ private function compileSection(BlockStatement $block, string $var, string $esca
209208
assert($block->program !== null);
210209

211210
$blockFn = $this->compileProgramWithBlockParams($block->program);
212-
$else = $this->compileElseClause($block);
211+
$else = $this->compileProgramOrNull($block->inverse);
213212

214213
if ($this->context->options->knownHelpersOnly) {
215214
return self::getRuntimeFunc('sec', "\$cx, $var, \$in, $blockFn, $else");
@@ -291,26 +290,29 @@ private function compileProgramWithBlockParams(Program $program): string
291290
private function compileBlockHelper(BlockStatement $block, string $name): string
292291
{
293292
$inverted = $block->program === null;
294-
if ($inverted) {
295-
assert($block->inverse !== null);
296-
}
297293
// For inverted blocks the fn body comes from the inverse program; for normal blocks, the program.
298-
$fnProgram = $inverted ? $block->inverse : $block->program;
294+
$fnProgram = $block->program ?? $block->inverse;
295+
assert($fnProgram !== null);
299296

300297
// Inline if/unless as ternary — eliminates hbbch dispatch and HelperOptions allocation.
301298
// Safe because if/unless don't change scope, so $cx and $in are already correct.
302299
// Negate for 'unless' in a normal block, or 'if' in an inverted block (swapped semantics).
303300
if ($this->canInlineConditional($block, $name, $fnProgram->blockParams)) {
304301
$cond = $this->compileConditionalExpr($block->params[0], $name === ($inverted ? 'if' : 'unless'));
305302
$body = $this->compileProgram($fnProgram);
306-
$elseBody = $inverted ? "''" : $this->compileProgramOrEmpty($block->inverse);
303+
$elseBody = $this->compileProgramOrEmpty($inverted ? null : $block->inverse);
307304
return "($cond ? $body : $elseBody)";
308305
}
309306

310307
$blockFn = $this->compileProgramWithBlockParams($fnProgram);
311-
[$fn, $else] = $inverted
312-
? ['null', $blockFn]
313-
: [$blockFn, $this->compileElseClause($block)];
308+
if ($inverted) {
309+
// no {{else}} clause, so there is nothing to compile for fn
310+
$fn = 'null';
311+
$else = $blockFn;
312+
} else {
313+
$fn = $blockFn;
314+
$else = $this->compileProgramOrNull($block->inverse);
315+
}
314316

315317
$outerBp = $this->outerBlockParamsExpr();
316318
$params = $this->compileParams($block->params, $block->hash);
@@ -372,7 +374,7 @@ private function compileDynamicBlockHelper(BlockStatement $block, string $name,
372374
$blockFn = $block->program !== null
373375
? $this->compileProgramWithBlockParams($block->program)
374376
: 'null';
375-
$else = $this->compileElseClause($block);
377+
$else = $this->compileProgramOrNull($block->inverse);
376378
$outerBp = $this->outerBlockParamsExpr();
377379
$helperName = self::quote($name);
378380
return self::getRuntimeFunc('dynhbbch', "\$cx, $helperName, $varPath, $params, \$in, $blockFn, $else, " . count($bp) . ", $outerBp");
@@ -385,7 +387,7 @@ private function DecoratorBlock(BlockStatement $block): string
385387
if ($helperName !== 'inline') {
386388
throw new \Exception('Unknown decorator: "' . $helperName . '"');
387389
} elseif (!$block->params) {
388-
$partialName = 'undefined';
390+
$partialName = 'undefined'; // match JS for {{#*inline}} without a name (params[0] on empty array is undefined)
389391
} else {
390392
$firstArg = $block->params[0];
391393
if (!$firstArg instanceof Literal) {
@@ -556,16 +558,6 @@ private function MustacheStatement(MustacheStatement $mustache): string
556558
return self::getRuntimeFunc($fn, "\$in[$escapedKey] ?? $miss");
557559
}
558560

559-
private function ContentStatement(ContentStatement $statement): string
560-
{
561-
return self::quote($statement->value);
562-
}
563-
564-
private function CommentStatement(CommentStatement $statement): string
565-
{
566-
return '';
567-
}
568-
569561
// ── Expressions ─────────────────────────────────────────────────
570562

571563
private function SubExpression(SubExpression $expression): string
@@ -604,7 +596,8 @@ private function PathExpression(PathExpression $expression): string
604596
$stringParts = $expression->tail;
605597
} else {
606598
$base = $this->buildBasePath($data, $depth);
607-
$stringParts = self::stringPartsOf($parts);
599+
/** @var string[] $parts */
600+
$stringParts = $parts;
608601
}
609602

610603
// `this` with no parts or empty parts
@@ -652,31 +645,6 @@ private function PathExpression(PathExpression $expression): string
652645
return $this->compileModeAwareLookup($base, $stringParts, $expression->original, $miss);
653646
}
654647

655-
private function StringLiteral(StringLiteral $literal): string
656-
{
657-
return self::quote($literal->value);
658-
}
659-
660-
private function NumberLiteral(NumberLiteral $literal): string
661-
{
662-
return (string) $literal->value;
663-
}
664-
665-
private function BooleanLiteral(BooleanLiteral $literal): string
666-
{
667-
return $literal->value ? 'true' : 'false';
668-
}
669-
670-
private function UndefinedLiteral(UndefinedLiteral $literal): string
671-
{
672-
return 'null';
673-
}
674-
675-
private function NullLiteral(NullLiteral $literal): string
676-
{
677-
return 'null';
678-
}
679-
680648
/**
681649
* Get the string key name for a literal used in path (mustache/block) position.
682650
* e.g. {{12}} looks up $in['12'], {{"foo bar"}} looks up $in['foo bar'], {{true}} looks up $in['true'].
@@ -848,18 +816,6 @@ private function getSimpleHelperName(PathExpression|Literal $path): ?string
848816
return $path->parts[0];
849817
}
850818

851-
/**
852-
* Compile the else/inverse clause of a block as a trailing closure argument, or 'null' if absent.
853-
*/
854-
private function compileElseClause(BlockStatement $block): string
855-
{
856-
if (!$block->inverse) {
857-
$this->lastCompileProgramHadDirectBpRef = false;
858-
return 'null';
859-
}
860-
return $this->compileProgramWithBlockParams($block->inverse);
861-
}
862-
863819
/**
864820
* Build the base path expression for a given data flag and depth.
865821
*/
@@ -929,6 +885,15 @@ private function missValue(string $key): string
929885
: 'null';
930886
}
931887

888+
private function compileProgramOrNull(?Program $program): string
889+
{
890+
if (!$program) {
891+
$this->lastCompileProgramHadDirectBpRef = false;
892+
return 'null';
893+
}
894+
return $this->compileProgramWithBlockParams($program);
895+
}
896+
932897
private function compileProgramOrEmpty(?Program $program): string
933898
{
934899
if (!$program) {
@@ -986,14 +951,4 @@ private function buildInlineHelperCall(string $name, array $params, ?Hash $hash)
986951
}
987952
return self::getRuntimeFunc('dynhbch', "\$cx, $helperName, $compiledParams, \$in");
988953
}
989-
990-
/**
991-
* Return only the string parts of a mixed parts array, re-indexed.
992-
* @param array<string|SubExpression> $parts
993-
* @return list<string>
994-
*/
995-
private static function stringPartsOf(array $parts): array
996-
{
997-
return array_values(array_filter($parts, fn($p) => is_string($p)));
998-
}
999954
}

tests/ErrorTest.php

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010

1111
/**
1212
* @phpstan-type RenderTest array{
13-
* template: string, options?: Options, helpers?: array<string, \Closure>, data?: array<mixed>, expected: string,
13+
* template: string, expected: string, desc?: string, options?: Options,
14+
* helpers?: array<string, \Closure>, data?: array<mixed>,
1415
* }
1516
* @phpstan-type ErrorCase array{template: string, options?: Options, expected: string}
1617
*/
@@ -24,19 +25,18 @@ class ErrorTest extends TestCase
2425
public function testRenderingException(
2526
string $template,
2627
string $expected,
28+
string $desc = '',
2729
?Options $options = null,
2830
array $helpers = [],
2931
array $data = [],
3032
): void {
3133
$php = Handlebars::precompile($template, $options ?? new Options());
3234
$renderer = Handlebars::template($php);
3335
try {
34-
$renderer($data, [
35-
'helpers' => $helpers,
36-
]);
37-
$this->fail("Expected to throw exception: {$expected}. CODE: $php");
36+
$result = $renderer($data, ['helpers' => $helpers]);
37+
$this->fail("$desc\nExpected exception: {$expected}\nRendered: $result\nPHP code:\n$php");
3838
} catch (\Exception $e) {
39-
$this->assertSame($expected, $e->getMessage(), $php);
39+
$this->assertSame($expected, $e->getMessage(), "$desc\nPHP code:\n$php");
4040
}
4141
}
4242

@@ -105,14 +105,14 @@ public static function renderErrorProvider(): array
105105
'expected' => '"foo" not defined',
106106
],
107107
[
108-
// strict mode should override helperMissing
108+
'desc' => 'strict mode should override helperMissing',
109109
'template' => '{{foo}}',
110110
'options' => new Options(strict: true),
111111
'helpers' => ['helperMissing' => fn() => 'bad'],
112112
'expected' => '"foo" not defined',
113113
],
114114
[
115-
// strict mode should override blockHelperMissing
115+
'desc' => 'strict mode should override blockHelperMissing',
116116
'template' => '{{#foo}}OK{{/foo}}',
117117
'options' => new Options(strict: true),
118118
'helpers' => ['blockHelperMissing' => fn() => 'bad'],
@@ -151,8 +151,8 @@ public static function renderErrorProvider(): array
151151
],
152152
'expected' => 'Expect the unexpected',
153153
],
154-
// ensure that callable strings in data aren't treated as functions
155154
[
155+
'desc' => "callable strings in data should not be treated as functions",
156156
'template' => "{{#foo.bar 'arg'}}{{/foo.bar}}",
157157
'data' => ['foo' => ['bar' => 'strlen']],
158158
'expected' => 'Missing helper: "foo.bar"',
@@ -168,9 +168,7 @@ public static function renderErrorProvider(): array
168168
[
169169
'template' => '{{test_join (foo bar)}}',
170170
'helpers' => [
171-
'test_join' => function ($input) {
172-
return join('.', $input);
173-
},
171+
'test_join' => fn($input) => join('.', $input),
174172
],
175173
'expected' => 'Missing helper: "foo"',
176174
],
@@ -194,18 +192,22 @@ public static function renderErrorProvider(): array
194192
'template' => '{{#each}}OK!{{/each}}',
195193
'expected' => 'Must pass iterator to #each',
196194
],
195+
[
196+
'desc' => 'strict mode should throw for missing block param property',
197+
'template' => '{{#each items as |item|}}{{item.missing}}{{/each}}',
198+
'options' => new Options(strict: true),
199+
'data' => ['items' => [['val' => 'x']]],
200+
'expected' => '"item.missing" not defined',
201+
],
197202
];
198203
}
199204

200205
#[DataProvider("errorProvider")]
201206
public function testErrors(string $template, string $expected, ?Options $options = null): void
202207
{
203-
try {
204-
Handlebars::precompile($template, $options ?? new Options());
205-
$this->fail("Expected to throw exception: {$expected}");
206-
} catch (\Exception $e) {
207-
$this->assertSame($expected, $e->getMessage());
208-
}
208+
$this->expectException(\Exception::class);
209+
$this->expectExceptionMessage($expected);
210+
Handlebars::precompile($template, $options ?? new Options());
209211
}
210212

211213
/**

0 commit comments

Comments
 (0)