Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion filters.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ var filters = {
},
default: (v, arg) => (isTruthy(v) ? v : arg),
divided_by: (v, arg) => divide(v, arg),
downcase: v => v.toLowerCase(),
downcase: v => (v == null ? "" : String(v).toLowerCase()),
escape: escape,
escape_once: str => escape(unescape(str)),
first: v => v[0],
Expand Down
14 changes: 12 additions & 2 deletions src/scope.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,15 @@ var Scope = {
assert(j !== -1, `unbalanced []: ${str}`)
name = str.slice(i + 1, j)
if (!lexical.isInteger(name)) { // foo[bar] vs. foo[1]
name = this.get(name)
// FIX: this.get() may return undefined when the variable is not
// in scope (strict_variables=false swallows the error). Convert
// undefined/null to an empty string so that push() does not crash
// attempting to read .length on undefined. An empty name is
// skipped by push(), which means the bracket-access expression
// resolves to its parent object rather than a property — this is
// consistent with standard Liquid's nil-safe property access.
var resolved = this.get(name)
name = (resolved === undefined || resolved === null) ? '' : String(resolved)
}
push()
i = j + 1
Expand All @@ -116,7 +124,9 @@ var Scope = {
return seq

function push () {
if (name.length) seq.push(name)
// Guard against undefined/null `name` (can arise when this.get() returns
// undefined for an unresolved variable inside bracket notation).
if (name !== null && name !== undefined && name.length) seq.push(name)
name = ''
}
}
Expand Down
23 changes: 23 additions & 0 deletions test/scope.js
Original file line number Diff line number Diff line change
Expand Up @@ -231,3 +231,26 @@ describe('scope', function () {
})
})
})

// ─── Regression: bracket access with undefined variable ───────────────────
describe('propertyAccessSeq regression: bracket access with undefined variable', function () {
var scope2
before(function () {
var Scope = require('../src/scope')
scope2 = Scope.factory({ unit_mapping: { VWO: 'Unique Visitors / Term' } })
})

it('should not throw when the bracket variable is undefined (not in scope)', function () {
// self is not in scope, so self.sd_product resolves to nil;
// before the fix this crashed with: "Cannot read properties of undefined (reading 'length')"
expect(function () {
scope2.propertyAccessSeq('unit_mapping[self.sd_product]')
}).to.not.throw()
})

it('should return empty seq when bracket variable resolves to nil', function () {
// With name="" (nil), push() skips it; seq only contains 'unit_mapping'
var seq = scope2.propertyAccessSeq('unit_mapping[self.sd_product]')
expect(seq[0]).to.equal('unit_mapping')
})
})