diff --git a/filters.js b/filters.js index 4742630d37..0a098fa0ff 100644 --- a/filters.js +++ b/filters.js @@ -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], diff --git a/src/scope.js b/src/scope.js index 9c46863f45..1e0bac9c11 100644 --- a/src/scope.js +++ b/src/scope.js @@ -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 @@ -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 = '' } } diff --git a/test/scope.js b/test/scope.js index 17ed436ab9..bee47f4a84 100644 --- a/test/scope.js +++ b/test/scope.js @@ -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') + }) +})