Fix errors with strict lookup in compat mode (#2150)

Fixes #2149, fixes #1741
This commit is contained in:
Theodore Brown
2026-04-21 14:30:55 -05:00
committed by GitHub
parent 0e1d35582a
commit 3105ca73c1
3 changed files with 94 additions and 7 deletions
+21 -7
View File
@@ -1175,20 +1175,34 @@ function strictLookup(requireTerminal, compiler, parts, startPartIndex, type) {
stack = compiler.nameLookup(stack, parts[i], type);
}
if (requireTerminal) {
if (!requireTerminal) {
return stack;
}
if (startPartIndex > len) {
// Compat mode already consumed all parts via depthedLookup; the stack
// holds the resolved value, not an object to look the property up on.
// Use container.strictLookup to traverse depths with a strict error on miss.
return [
compiler.aliasable('container.strict'),
'(',
stack,
', ',
compiler.aliasable('container.strictLookup'),
'(depths, ',
compiler.quotedString(parts[len]),
', ',
JSON.stringify(compiler.source.currentLocation),
' )',
];
} else {
return stack;
}
return [
compiler.aliasable('container.strict'),
'(',
stack,
', ',
compiler.quotedString(parts[len]),
', ',
JSON.stringify(compiler.source.currentLocation),
' )',
];
}
export default JavaScriptCompiler;
+16
View File
@@ -116,6 +116,22 @@ export function template(templateSpec, env) {
}
return container.lookupProperty(obj, name);
},
strictLookup: function (depths, name, loc) {
const len = depths.length;
let depth;
for (let i = 0; i < len; i++) {
const d = depths[i];
if (
d &&
(typeof d === 'object' || typeof d === 'function') &&
name in d
) {
depth = d;
break;
}
}
return container.strict(depth, name, loc);
},
lookupProperty: function (parent, propertyName) {
if (Utils.isMap(parent)) {
return parent.get(propertyName);
+57
View File
@@ -121,6 +121,63 @@ describe('strict', function () {
});
});
describe('strict and compat mode', function () {
it('GH-1741: should render a simple variable', function () {
expectTemplate('{{v}}')
.withCompileOptions({ strict: true, compat: true })
.withInput({ v: 'a' })
.toCompileTo('a');
});
it('should throw for a missing variable', function () {
expectTemplate('{{v}}')
.withCompileOptions({ strict: true, compat: true })
.toThrow(Exception, /"v" not defined in/);
});
it('GH-2149: should render correctly when block context is a boolean', function () {
expectTemplate('{{#foo}}Hello {{bar}}{{/foo}}')
.withCompileOptions({ strict: true, compat: true })
.withInput({ foo: true, bar: 'World' })
.toCompileTo('Hello World');
});
it('GH-2149: should render correctly when looking up a property on each item', function () {
expectTemplate('{{#each items}}{{name}}{{/each}}')
.withCompileOptions({ strict: true, compat: true })
.withInput({ items: [{ name: 'Hello' }] })
.toCompileTo('Hello');
});
it('should still throw when a property is missing at all depths', function () {
expectTemplate('{{#each items}}{{name}}{{/each}}')
.withCompileOptions({ strict: true, compat: true })
.withInput({ items: [1, 2] })
.toThrow(Exception, /"name" not defined in/);
});
it('should still perform recursive lookup when a property is not in the current context', function () {
expectTemplate('{{#each items}}{{name}}{{/each}}')
.withCompileOptions({ strict: true, compat: true })
.withInput({ name: 'root', items: [{}] })
.toCompileTo('root');
});
it('should still perform recursive lookup with a multi-part path not in context', function () {
expectTemplate('{{#with child}}{{name.first}}{{/with}}')
.withCompileOptions({ strict: true, compat: true })
.withInput({ name: { first: 'root' }, child: { name: null } })
.toCompileTo('root');
});
it('should directly return an explicitly null property', function () {
expectTemplate('{{#each items}}{{name}}{{/each}}')
.withCompileOptions({ strict: true, compat: true })
.withInput({ name: 'root', items: [{ name: null }] })
.toCompileTo('');
});
});
describe('assume objects', function () {
it('should ignore missing property', function () {
expectTemplate('{{hello}}')