From 205a4520231498cdf0fe1a7029628007f578c61c Mon Sep 17 00:00:00 2001 From: Jason Mobley Date: Tue, 11 Aug 2026 15:19:58 -0400 Subject: [PATCH 1/2] Fix Parser.name() to treat '}' as a terminator for shorthand properties An ES6 shorthand object property immediately followed by '}' (e.g. `{a}`) failed to parse because the terminator set in name() didn't include '}', causing the scanner to consume it and run past the end of the object literal. Add regression tests covering the bare-brace case plus its variants in function-call args and array elements. --- packages/utils.parser/spec/parserBehaviors.ts | 35 +++++++++++++++++++ packages/utils.parser/src/Parser.ts | 2 +- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/packages/utils.parser/spec/parserBehaviors.ts b/packages/utils.parser/spec/parserBehaviors.ts index f3554ea9d..dd0db1e4f 100644 --- a/packages/utils.parser/spec/parserBehaviors.ts +++ b/packages/utils.parser/spec/parserBehaviors.ts @@ -134,6 +134,41 @@ describe('the bindings parser', function () { assert.equal(bindings.attr().kv, 'Sam') }) + describe('shorthand (ES6) object properties', function () { + it('parses a shorthand property immediately followed by }', function () { + const bindings = new Parser().parse('x:{a}', ctxStub({ a: 1 })) + assert.deepEqual(Object.keys(bindings.x()), ['a']) + assert.equal(bindings.x().a, 1) + }) + + it('parses multiple shorthand properties with no trailing comma', function () { + const bindings = new Parser().parse('x:{a,b,c}', ctxStub({ a: 1, b: 2, c: 3 })) + assert.deepEqual(Object.keys(bindings.x()), ['a', 'b', 'c']) + }) + + it('parses a shorthand property mixed with explicit key:value pairs', function () { + const bindings = new Parser().parse('x:{a:1,b}', ctxStub({ b: 2 })) + assert.deepEqual(Object.keys(bindings.x()), ['a', 'b']) + assert.equal(bindings.x().b, 2) + }) + + it('still parses a trailing comma after a shorthand property (regression guard)', function () { + const bindings = new Parser().parse('x:{a,}', ctxStub({ a: 1 })) + assert.deepEqual(Object.keys(bindings.x()), ['a']) + }) + + it('parses a shorthand-object literal as a bare function-call argument', function () { + const foo = (o: any) => o.a + const bindings = new Parser().parse('x: foo({a})', ctxStub({ foo, a: 1 })) + assert.equal(bindings.x(), 1) + }) + + it('parses a shorthand-object literal as a bare array element', function () { + const bindings = new Parser().parse('x: [{a}]', ctxStub({ a: 1 })) + assert.deepEqual(Object.keys(bindings.x()[0]), ['a']) + }) + }) + it('parses object: attr: {name: observable(value)}', function () { const binding = 'attr : { klass: kValue }', context = ctxStub({ kValue: observable('Gollum') }), diff --git a/packages/utils.parser/src/Parser.ts b/packages/utils.parser/src/Parser.ts index 7b9860950..364344089 100644 --- a/packages/utils.parser/src/Parser.ts +++ b/packages/utils.parser/src/Parser.ts @@ -133,7 +133,7 @@ export default class Parser { this.error('Object name: ' + name + ' missing closing ' + enclosedBy) } return name - } else if (ch === ':' || ch <= ' ' || ch === ',' || ch === '|') { + } else if (ch === ':' || ch <= ' ' || ch === ',' || ch === '|' || ch === '}') { return name } name += ch From e5b6cfb4aad853f0e97c2a9cebbdf5908498d281 Mon Sep 17 00:00:00 2001 From: Jason Mobley Date: Wed, 12 Aug 2026 15:05:29 -0400 Subject: [PATCH 2/2] Guard name() terminator check with !enclosedBy The terminator characters (':', whitespace, ',', '|', '}') only mark the end of an unquoted name. Without the !enclosedBy guard, any of these characters occurring inside a quoted name (e.g. "a}b", "a b", "a,b") would end the name early, since the check ran unconditionally instead of only when not inside a quoted string. This predates the prior '}' addition -- space/comma/pipe already had the same defect. --- packages/utils.parser/spec/parserBehaviors.ts | 42 +++++++++++++++++++ packages/utils.parser/src/Parser.ts | 2 +- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/packages/utils.parser/spec/parserBehaviors.ts b/packages/utils.parser/spec/parserBehaviors.ts index dd0db1e4f..cd858a369 100644 --- a/packages/utils.parser/spec/parserBehaviors.ts +++ b/packages/utils.parser/spec/parserBehaviors.ts @@ -169,6 +169,48 @@ describe('the bindings parser', function () { }) }) + describe('quoted binding names containing terminator characters', function () { + // name() must not stop at ':', whitespace, ',', '|', or '}' while inside + // a quoted (enclosedBy) name -- those characters only terminate an + // unquoted name; a quote should keep scanning through to its closing quote. + it('parses a double-quoted name containing }', function () { + const bindings = new Parser().parse('"a}b": 1') + assert.deepEqual(Object.keys(bindings), ['a}b']) + assert.equal(bindings['a}b'](), 1) + }) + + it('parses a single-quoted name containing }', function () { + const bindings = new Parser().parse("'a}b': 1") + assert.deepEqual(Object.keys(bindings), ['a}b']) + assert.equal(bindings['a}b'](), 1) + }) + + it('parses a quoted name containing a space', function () { + const bindings = new Parser().parse('"a b": 1') + assert.deepEqual(Object.keys(bindings), ['a b']) + assert.equal(bindings['a b'](), 1) + }) + + it('parses a quoted name containing a comma', function () { + const bindings = new Parser().parse('"a,b": 1, c: 2') + assert.deepEqual(Object.keys(bindings), ['a,b', 'c']) + assert.equal(bindings['a,b'](), 1) + assert.equal(bindings.c(), 2) + }) + + it('parses a quoted name containing a pipe', function () { + const bindings = new Parser().parse('"a|b": 1') + assert.deepEqual(Object.keys(bindings), ['a|b']) + assert.equal(bindings['a|b'](), 1) + }) + + it('parses a quoted name containing a colon', function () { + const bindings = new Parser().parse('"a:b": 1') + assert.deepEqual(Object.keys(bindings), ['a:b']) + assert.equal(bindings['a:b'](), 1) + }) + }) + it('parses object: attr: {name: observable(value)}', function () { const binding = 'attr : { klass: kValue }', context = ctxStub({ kValue: observable('Gollum') }), diff --git a/packages/utils.parser/src/Parser.ts b/packages/utils.parser/src/Parser.ts index 364344089..65a20e3b6 100644 --- a/packages/utils.parser/src/Parser.ts +++ b/packages/utils.parser/src/Parser.ts @@ -133,7 +133,7 @@ export default class Parser { this.error('Object name: ' + name + ' missing closing ' + enclosedBy) } return name - } else if (ch === ':' || ch <= ' ' || ch === ',' || ch === '|' || ch === '}') { + } else if (!enclosedBy && (ch === ':' || ch <= ' ' || ch === ',' || ch === '|' || ch === '}')) { return name } name += ch