Skip to content

Commit 9025895

Browse files
committed
Better align compat mode lookup with Mustache
I also opened handlebars-lang/handlebars.js#2151 to apply the same fix to Handlebars.js.
1 parent 093e6c9 commit 9025895

3 files changed

Lines changed: 15 additions & 37 deletions

File tree

src/Compiler.php

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -823,15 +823,12 @@ private function compileModeAwareLookup(string $base, array $parts, string $orig
823823
return $base;
824824
}
825825
// Compat mode: walk the scope chain for parts[0] instead of looking up in $in directly.
826-
// For single-part strict paths, use compatStrictLookup (throws on miss); all other paths
827-
// use compatLookup (null falls through to the next depth), matching HBS.js container.lookup.
826+
// For single-part strict paths, pass true to throw on miss; otherwise returns null.
828827
if ($this->options->compat && $base === '$in' && !$scoped) {
829828
$escapedName = self::quote($parts[0]);
830829
array_shift($parts);
831-
$lookupFn = (!$parts && $this->options->strict && !$this->compilingHelperArgs)
832-
? 'compatStrictLookup'
833-
: 'compatLookup';
834-
$base = self::getRuntimeFunc($lookupFn, '$cx, $in, ' . $escapedName);
830+
$strictArg = (!$parts && $this->options->strict && !$this->compilingHelperArgs) ? ', true' : '';
831+
$base = self::getRuntimeFunc('compatLookup', '$cx, $in, ' . $escapedName . $strictArg);
835832
if (!$parts) {
836833
return $base;
837834
}

src/Runtime.php

Lines changed: 10 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -230,9 +230,7 @@ public static function invokeAmbiguous(RuntimeContext $cx, string $name, mixed &
230230
$value = $cx->helpers[$name] ?? null;
231231
if ($value === null) {
232232
if ($compat) {
233-
$value = $strict
234-
? self::compatStrictLookup($cx, $_this, $name)
235-
: self::compatLookup($cx, $_this, $name);
233+
$value = self::compatLookup($cx, $_this, $name, $strict);
236234
} elseif ($strict) {
237235
$value = self::strictLookup($_this, $name, $name);
238236
} else {
@@ -249,40 +247,23 @@ public static function invokeAmbiguous(RuntimeContext $cx, string $name, mixed &
249247
}
250248

251249
/**
252-
* Non-strict compat depths-walk for a single key, equivalent to HBS.js container.lookup(depths, name).
250+
* Compat depths-walk for a single key, equivalent to HBS.js container.lookup / container.strictLookup.
253251
* Checks $in first, then walks $cx->depths from the closest ancestor outward.
254-
* Null values are skipped (treated as not found), matching HBS.js container.lookup's `result != null` check.
252+
* Uses array_key_exists so explicit null values shadow parent contexts (Mustache semantics).
253+
* When $strict is true, throws for a key absent from all frames; otherwise returns null.
255254
*/
256-
public static function compatLookup(RuntimeContext $cx, mixed $in, string $name): mixed
257-
{
258-
if (is_array($in) && ($value = $in[$name] ?? null) !== null) {
259-
return $value;
260-
}
261-
for ($i = count($cx->depths) - 1; $i >= 0; $i--) {
262-
$ctx = $cx->depths[$i];
263-
if (is_array($ctx) && ($v = $ctx[$name] ?? null) !== null) {
264-
return $v;
265-
}
266-
}
267-
return null;
268-
}
269-
270-
/**
271-
* Strict compat depths-walk for a single key, equivalent to HBS.js container.strictLookup(depths, name, loc).
272-
* Checks $in first, then walks $cx->depths from the closest ancestor outward.
273-
* Null values are treated as defined (array_key_exists), matching HBS.js container.strictLookup's `name in d` check.
274-
*/
275-
public static function compatStrictLookup(RuntimeContext $cx, mixed $in, string $name): mixed
255+
public static function compatLookup(RuntimeContext $cx, mixed $in, string $name, bool $strict = false): mixed
276256
{
277257
if (is_array($in) && array_key_exists($name, $in)) {
278-
return self::strictLookup($in, $name, $name);
258+
return $in[$name];
279259
}
280260
for ($i = count($cx->depths) - 1; $i >= 0; $i--) {
281-
if (is_array($cx->depths[$i]) && array_key_exists($name, $cx->depths[$i])) {
282-
return self::strictLookup($cx->depths[$i], $name, $name);
261+
$ctx = $cx->depths[$i];
262+
if (is_array($ctx) && array_key_exists($name, $ctx)) {
263+
return $ctx[$name];
283264
}
284265
}
285-
return self::strictLookup(null, $name, $name);
266+
return $strict ? self::strictLookup(null, $name, $name) : null;
286267
}
287268

288269
/**

tests/RegressionTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2171,10 +2171,10 @@ public static function contextProvider(): array
21712171
'data' => ['name' => 'root', 'items' => [[]]],
21722172
'expected' => 'root',
21732173
],
2174-
'compat+strict: missing multi-part path uses depths walk for first segment' => [
2174+
'compat+strict: should still perform recursive lookup with a multi-part path not in context' => [
21752175
'template' => '{{#with child}}{{name.first}}{{/with}}',
21762176
'options' => new Options(compat: true, strict: true),
2177-
'data' => ['name' => ['first' => 'root'], 'child' => ['name' => null]],
2177+
'data' => ['name' => ['first' => 'root'], 'child' => ['x' => 'y']],
21782178
'expected' => 'root',
21792179
],
21802180
'compat+strict: explicitly null property in context returned directly' => [

0 commit comments

Comments
 (0)