From 529de54c73b12922f5617ab404d2ba3aa4f895a7 Mon Sep 17 00:00:00 2001 From: Neo Date: Mon, 15 Jun 2026 07:37:40 +0000 Subject: [PATCH] fix: nil-safe bracket access in propertyAccessSeq + nil-safe downcase filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two related bugs causing ComputationError when Liquid computation templates use bracket notation (e.g. unit_mapping[self.sd_product]) outside a computeColumn block, so that the bracket variable resolves to nil. Bug 1 (src/scope.js — propertyAccessSeq): When this.get(name) returns undefined (strict_variables=false swallows 'undefined variable' error), the local variable is set to undefined. The inner push() helper then crashes: throws 'Cannot read properties of undefined (reading "length")'. Fix: coerce nil result to empty string before push(); empty name is safely skipped by push(), preventing the crash. Bug 2 (filters.js — downcase): downcase was defined as v => v.toLowerCase(). When v is nil/undefined (e.g. self.sd_product when self is not in scope) this crashes with 'Cannot read properties of undefined (reading "toLowerCase")'. Fix: guard with null check, returning empty string for nil input. Root template issue (addressed separately by CSM): The sd_order_details computed fields (sd_ap, sd_cp, sd_unit) in Workflow 1401, Workspace 3409 (Wingify/VWO) are missing {% computeColumn %} wrappers. Without the wrapper, self is never set per-row, making self.sd_product nil. All three computed fields should be wrapped in: {% computeColumn sd_order_details FIELD_NAME %}...{% endcomputeColumn %} Fixes: VWO/Wingify wsid=3409 workflow_id=1401 contract creation failure Sentry: 6765108933 Co-authored-by: Anusree Raj LS --- filters.js | 2 +- src/scope.js | 14 ++++++++++++-- test/scope.js | 23 +++++++++++++++++++++++ 3 files changed, 36 insertions(+), 3 deletions(-) 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') + }) +})