Skip to content

Commit cd47c94

Browse files
committed
Fix errors with strict lookup in compat mode
Fixes #2149, fixes #1741
1 parent 0e1d355 commit cd47c94

3 files changed

Lines changed: 94 additions & 7 deletions

File tree

lib/handlebars/compiler/javascript-compiler.js

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1175,20 +1175,34 @@ function strictLookup(requireTerminal, compiler, parts, startPartIndex, type) {
11751175
stack = compiler.nameLookup(stack, parts[i], type);
11761176
}
11771177

1178-
if (requireTerminal) {
1178+
if (!requireTerminal) {
1179+
return stack;
1180+
}
1181+
1182+
if (startPartIndex > len) {
1183+
// Compat mode already consumed all parts via depthedLookup; the stack
1184+
// holds the resolved value, not an object to look the property up on.
1185+
// Use container.strictLookup to traverse depths with a strict error on miss.
11791186
return [
1180-
compiler.aliasable('container.strict'),
1181-
'(',
1182-
stack,
1183-
', ',
1187+
compiler.aliasable('container.strictLookup'),
1188+
'(depths, ',
11841189
compiler.quotedString(parts[len]),
11851190
', ',
11861191
JSON.stringify(compiler.source.currentLocation),
11871192
' )',
11881193
];
1189-
} else {
1190-
return stack;
11911194
}
1195+
1196+
return [
1197+
compiler.aliasable('container.strict'),
1198+
'(',
1199+
stack,
1200+
', ',
1201+
compiler.quotedString(parts[len]),
1202+
', ',
1203+
JSON.stringify(compiler.source.currentLocation),
1204+
' )',
1205+
];
11921206
}
11931207

11941208
export default JavaScriptCompiler;

lib/handlebars/runtime.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,22 @@ export function template(templateSpec, env) {
116116
}
117117
return container.lookupProperty(obj, name);
118118
},
119+
strictLookup: function (depths, name, loc) {
120+
const len = depths.length;
121+
let depth;
122+
for (let i = 0; i < len; i++) {
123+
const d = depths[i];
124+
if (
125+
d &&
126+
(typeof d === 'object' || typeof d === 'function') &&
127+
name in d
128+
) {
129+
depth = d;
130+
break;
131+
}
132+
}
133+
return container.strict(depth, name, loc);
134+
},
119135
lookupProperty: function (parent, propertyName) {
120136
if (Utils.isMap(parent)) {
121137
return parent.get(propertyName);

spec/strict.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,63 @@ describe('strict', function () {
121121
});
122122
});
123123

124+
describe('strict and compat mode', function () {
125+
it('GH-1741: should render a simple variable', function () {
126+
expectTemplate('{{v}}')
127+
.withCompileOptions({ strict: true, compat: true })
128+
.withInput({ v: 'a' })
129+
.toCompileTo('a');
130+
});
131+
132+
it('should throw for a missing variable', function () {
133+
expectTemplate('{{v}}')
134+
.withCompileOptions({ strict: true, compat: true })
135+
.toThrow(Exception, /"v" not defined in/);
136+
});
137+
138+
it('GH-2149: should render correctly when block context is a boolean', function () {
139+
expectTemplate('{{#foo}}Hello {{bar}}{{/foo}}')
140+
.withCompileOptions({ strict: true, compat: true })
141+
.withInput({ foo: true, bar: 'World' })
142+
.toCompileTo('Hello World');
143+
});
144+
145+
it('GH-2149: should render correctly when looking up a property on each item', function () {
146+
expectTemplate('{{#each items}}{{name}}{{/each}}')
147+
.withCompileOptions({ strict: true, compat: true })
148+
.withInput({ items: [{ name: 'Hello' }] })
149+
.toCompileTo('Hello');
150+
});
151+
152+
it('should still throw when a property is missing at all depths', function () {
153+
expectTemplate('{{#each items}}{{name}}{{/each}}')
154+
.withCompileOptions({ strict: true, compat: true })
155+
.withInput({ items: [1, 2] })
156+
.toThrow(Exception, /"name" not defined in/);
157+
});
158+
159+
it('should still perform recursive lookup when a property is not in the current context', function () {
160+
expectTemplate('{{#each items}}{{name}}{{/each}}')
161+
.withCompileOptions({ strict: true, compat: true })
162+
.withInput({ name: 'root', items: [{}] })
163+
.toCompileTo('root');
164+
});
165+
166+
it('should still perform recursive lookup with a multi-part path not in context', function () {
167+
expectTemplate('{{#with child}}{{name.first}}{{/with}}')
168+
.withCompileOptions({ strict: true, compat: true })
169+
.withInput({ name: { first: 'root' }, child: { name: null } })
170+
.toCompileTo('root');
171+
});
172+
173+
it('should directly return an explicitly null property', function () {
174+
expectTemplate('{{#each items}}{{name}}{{/each}}')
175+
.withCompileOptions({ strict: true, compat: true })
176+
.withInput({ name: 'root', items: [{ name: null }] })
177+
.toCompileTo('');
178+
});
179+
});
180+
124181
describe('assume objects', function () {
125182
it('should ignore missing property', function () {
126183
expectTemplate('{{hello}}')

0 commit comments

Comments
 (0)