Skip to content

Commit 6815514

Browse files
committed
Fix helper calls in complex subexpressions
1 parent 2d525eb commit 6815514

2 files changed

Lines changed: 57 additions & 47 deletions

File tree

src/Compiler.php

Lines changed: 34 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -526,25 +526,8 @@ private function MustacheStatement(MustacheStatement $mustache): string
526526
$type = $this->classifySexpr($helperName, $mustache->params, $mustache->hash);
527527

528528
if ($type === SexprType::Helper) {
529-
if ($helperName !== null) {
530-
$call = $this->buildInlineHelperCall($helperName, $mustache->params, $mustache->hash);
531-
return self::getRuntimeFunc($fn, $call);
532-
}
533-
// Complex PathExpression with params: route through resolveHelper+hbch so helperMissing fires,
534-
// or dv() for data/depth/scoped paths where helper dispatch does not apply.
535-
if ($path instanceof PathExpression) {
536-
$varPath = $this->PathExpression($path);
537-
$stringParts = array_filter($path->parts, 'is_string');
538-
if (!$path->data && $path->depth === 0 && !self::scopedId($path)
539-
&& count($stringParts) === count($path->parts)) {
540-
$logicalName = self::quote(implode('.', $stringParts));
541-
$compiledParams = $this->compileParams($mustache->params, $mustache->hash);
542-
return self::getRuntimeFunc($fn, $this->buildResolvedHbchCall($logicalName, $varPath, $compiledParams));
543-
}
544-
$args = array_map(fn($p) => $this->compileExpression($p), $mustache->params);
545-
$call = self::getRuntimeFunc('dv', "$varPath, " . implode(', ', $args));
546-
return self::getRuntimeFunc($fn, $call);
547-
}
529+
$call = $this->compileHelperCall($helperName, $path, $mustache->params, $mustache->hash);
530+
return self::getRuntimeFunc($fn, $call);
548531
}
549532

550533
if ($helperName !== null && $type === SexprType::Ambiguous && !$this->context->options->strict) {
@@ -584,17 +567,7 @@ private function SubExpression(SubExpression $expression): string
584567
? $this->getSimpleHelperName($path)
585568
: null;
586569

587-
if ($helperName === null) {
588-
// Dynamic callable: path rooted at a sub-expression, e.g. ((helper).prop args)
589-
if ($path instanceof PathExpression) {
590-
$varPath = $this->PathExpression($path);
591-
$args = array_map(fn($p) => $this->compileExpression($p), $expression->params);
592-
return self::getRuntimeFunc('dv', implode(', ', [$varPath, ...$args]));
593-
}
594-
throw new \Exception('Sub-expression must be a helper call');
595-
}
596-
597-
return $this->buildInlineHelperCall($helperName, $expression->params, $expression->hash);
570+
return $this->compileHelperCall($helperName, $path, $expression->params, $expression->hash);
598571
}
599572

600573
private function PathExpression(PathExpression $expression): string
@@ -902,6 +875,37 @@ private function buildResolvedHbchCall(string $escapedName, string $varPath, str
902875
return self::getRuntimeFunc('hbch', "\$cx, $resolved, $escapedName, $params, \$in");
903876
}
904877

878+
/**
879+
* Compile a helper call for a MustacheStatement (Helper type) or SubExpression.
880+
* @param Expression[] $params
881+
*/
882+
private function compileHelperCall(?string $helperName, Expression $path, array $params, ?Hash $hash): string
883+
{
884+
if ($helperName !== null) {
885+
$compiledParams = $this->compileParams($params, $hash);
886+
$escapedName = self::quote($helperName);
887+
if ($this->isKnownHelper($helperName)) {
888+
return self::getRuntimeFunc('hbch', "\$cx, \$cx->helpers[$escapedName], $escapedName, $compiledParams, \$in");
889+
}
890+
if ($this->context->options->knownHelpersOnly) {
891+
$this->throwKnownHelpersOnly($helperName);
892+
}
893+
return $this->buildResolvedHbchCall($escapedName, "\$in[$escapedName] ?? null", $compiledParams);
894+
}
895+
if ($path instanceof PathExpression) {
896+
$varPath = $this->PathExpression($path);
897+
$stringParts = array_filter($path->parts, 'is_string');
898+
if (!$path->data && $path->depth === 0 && !self::scopedId($path)
899+
&& count($stringParts) === count($path->parts)) {
900+
$logicalName = self::quote(implode('.', $stringParts));
901+
return $this->buildResolvedHbchCall($logicalName, $varPath, $this->compileParams($params, $hash));
902+
}
903+
$args = array_map(fn($p) => $this->compileExpression($p), $params);
904+
return self::getRuntimeFunc('dv', implode(', ', [$varPath, ...$args]));
905+
}
906+
throw new \Exception('Sub-expression must be a helper call');
907+
}
908+
905909
/**
906910
* Build runtime function call.
907911
*/
@@ -964,21 +968,4 @@ private function throwKnownHelpersOnly(string $helperName): never
964968
{
965969
throw new \Exception("You specified knownHelpersOnly, but used the unknown helper $helperName");
966970
}
967-
968-
/**
969-
* Build an hbch inline helper call string.
970-
* @param Expression[] $params
971-
*/
972-
private function buildInlineHelperCall(string $name, array $params, ?Hash $hash): string
973-
{
974-
$compiledParams = $this->compileParams($params, $hash);
975-
$helperName = self::quote($name);
976-
if ($this->isKnownHelper($name)) {
977-
return self::getRuntimeFunc('hbch', "\$cx, \$cx->helpers[$helperName], $helperName, $compiledParams, \$in");
978-
}
979-
if ($this->context->options->knownHelpersOnly) {
980-
$this->throwKnownHelpersOnly($name);
981-
}
982-
return $this->buildResolvedHbchCall($helperName, "\$in[$helperName] ?? null", $compiledParams);
983-
}
984971
}

tests/RegressionTest.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2277,6 +2277,29 @@ public static function subexpressionPathProvider(): array
22772277
],
22782278
'expected' => 'WORLD',
22792279
],
2280+
2281+
'sub-expression: multi-segment callee invokes helperMissing when not in context' => [
2282+
'template' => '{{identity (foo.bar baz)}}',
2283+
'data' => ['baz' => 'val'],
2284+
'helpers' => [
2285+
'identity' => fn($x) => $x,
2286+
'helperMissing' => fn($x, HelperOptions $opts) => 'missing:' . $opts->name,
2287+
],
2288+
'expected' => 'missing:foo.bar',
2289+
],
2290+
'sub-expression: multi-segment callee receives HelperOptions' => [
2291+
'template' => '{{{identity (foo.bar baz)}}}',
2292+
'data' => [
2293+
'foo' => [
2294+
'bar' => function ($x, ?HelperOptions $opts = null) {
2295+
return 'name=' . ($opts->name ?? 'null') . ',x=' . $x;
2296+
},
2297+
],
2298+
'baz' => 'val',
2299+
],
2300+
'helpers' => ['identity' => fn($x) => $x],
2301+
'expected' => 'name=foo.bar,x=val',
2302+
],
22802303
];
22812304
}
22822305

0 commit comments

Comments
 (0)