mirror of
https://github.com/harttle/liquidjs.git
synced 2026-09-15 20:30:39 -07:00
feat(v11)!: restrict size/first/last magic keys to arrays and strings
Replace ownPropertyOnly guards with simpler v11 semantics: magic property resolution applies only to arrays and strings. Plain objects no longer get Object.keys size or first/last fallbacks. Related to #916 Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -32,7 +32,7 @@ describe('Context', function () {
|
||||
it('should read nested property', async function () {
|
||||
expect(ctx.get(['obj', 'first'])).toEqual('f')
|
||||
expect(ctx.get(['obj', 'last'])).toEqual('l')
|
||||
expect(ctx.get(['obj', 'size'])).toEqual(2)
|
||||
expect(ctx.get(['obj', 'size'])).toBeUndefined()
|
||||
})
|
||||
it('undefined property should yield undefined', async function () {
|
||||
expect(ctx.get(['notdefined'])).toEqual(undefined)
|
||||
@@ -55,7 +55,9 @@ describe('Context', function () {
|
||||
it('should return array length as size', async function () {
|
||||
expect(ctx.get(['bar', 'arr', 'size'])).toEqual(2)
|
||||
})
|
||||
it('should return map size as size', async function () {
|
||||
it('should return map size only via own property access', async function () {
|
||||
expect(ctx.get(['map', 'size'])).toBeUndefined()
|
||||
ctx = new Context(scope, { ownPropertyOnly: false } as any)
|
||||
expect(ctx.get(['map', 'size'])).toEqual(1)
|
||||
})
|
||||
it('should return undefined if not have a size', async function () {
|
||||
@@ -68,6 +70,14 @@ describe('Context', function () {
|
||||
it('should read .last of array', async function () {
|
||||
expect(ctx.get(['bar', 'arr', 'last'])).toEqual('b')
|
||||
})
|
||||
it('should read own size property on objects', async function () {
|
||||
expect(ctx.get(['zoo', 'size'])).toEqual(4)
|
||||
})
|
||||
it('should read string first/last as magic keys', async function () {
|
||||
ctx.push({ str: 'abc' })
|
||||
expect(ctx.getSync(['str', 'first'])).toEqual('a')
|
||||
expect(ctx.getSync(['str', 'last'])).toEqual('c')
|
||||
})
|
||||
it('should read element of array', async function () {
|
||||
expect(ctx.get(['arr', 1])).toEqual('b')
|
||||
})
|
||||
@@ -167,13 +177,16 @@ describe('Context', function () {
|
||||
ctx.push({ foo: [1, 2] })
|
||||
return expect(ctx.getSync(['foo', 'size'])).toEqual(2)
|
||||
})
|
||||
it('should allow size to access Set.prototype.size', function () {
|
||||
it('should allow size to access Set.prototype.size via property access', function () {
|
||||
ctx.push({ foo: new Set([1, 2]) })
|
||||
expect(ctx.getSync(['foo', 'size'])).toEqual(undefined)
|
||||
ctx = new Context({}, { ownPropertyOnly: false } as any)
|
||||
ctx.push({ foo: new Set([1, 2]) })
|
||||
return expect(ctx.getSync(['foo', 'size'])).toEqual(2)
|
||||
})
|
||||
it('should allow size to access Object key count', function () {
|
||||
it('should not apply size magic to plain objects', function () {
|
||||
ctx.push({ foo: { bar: 'BAR', coo: 'COO' } })
|
||||
return expect(ctx.getSync(['foo', 'size'])).toEqual(2)
|
||||
return expect(ctx.getSync(['foo', 'size'])).toEqual(undefined)
|
||||
})
|
||||
it('should throw when property is hidden and strictVariables is true', function () {
|
||||
ctx = new Context(ctx, {
|
||||
@@ -183,11 +196,11 @@ describe('Context', function () {
|
||||
ctx.push({ foo: Object.create({ bar: 'BAR' }) })
|
||||
return expect(() => ctx.getSync(['foo', 'bar'])).toThrow(/undefined variable: foo.bar/)
|
||||
})
|
||||
it('should block prototype size/first/last magic keys', function () {
|
||||
it('should not apply size/first/last magic keys to plain objects', function () {
|
||||
Object.assign(Object.prototype, { size: 123, first: 'FIRST_PROTO', last: 'LAST_PROTO' })
|
||||
try {
|
||||
ctx.push({ foo: {} })
|
||||
expect(ctx.getSync(['foo', 'size'])).toEqual(0)
|
||||
expect(ctx.getSync(['foo', 'size'])).toEqual(undefined)
|
||||
expect(ctx.getSync(['foo', 'first'])).toEqual(undefined)
|
||||
expect(ctx.getSync(['foo', 'last'])).toEqual(undefined)
|
||||
} finally {
|
||||
@@ -196,13 +209,13 @@ describe('Context', function () {
|
||||
delete (Object.prototype as any).last
|
||||
}
|
||||
})
|
||||
it('should allow array first/last/size magic keys with ownPropertyOnly', function () {
|
||||
it('should allow array first/last/size magic keys', function () {
|
||||
ctx.push({ foo: [1, 2, 3] })
|
||||
expect(ctx.getSync(['foo', 'first'])).toEqual(1)
|
||||
expect(ctx.getSync(['foo', 'last'])).toEqual(3)
|
||||
expect(ctx.getSync(['foo', 'size'])).toEqual(3)
|
||||
})
|
||||
it('should use prototype magic keys when ownPropertyOnly=false', function () {
|
||||
it('should read prototype size/first/last via property access when ownPropertyOnly=false', function () {
|
||||
Object.assign(Object.prototype, { size: 123, first: 'FIRST_PROTO', last: 'LAST_PROTO' })
|
||||
try {
|
||||
ctx = new Context({}, { ownPropertyOnly: false } as any)
|
||||
|
||||
+9
-15
@@ -129,9 +129,9 @@ export class Context {
|
||||
const value = readJSProperty(obj, key, this.ownPropertyOnly)
|
||||
if (value === undefined && obj instanceof Drop) return obj.liquidMethodMissing(key, this)
|
||||
if (isFunction(value)) return value.call(obj)
|
||||
if (key === 'size') return readSize(obj, this.ownPropertyOnly)
|
||||
else if (key === 'first') return readFirst(obj, this.ownPropertyOnly)
|
||||
else if (key === 'last') return readLast(obj, this.ownPropertyOnly)
|
||||
if (key === 'size' && (isArray(obj) || isString(obj))) return readSize(obj)
|
||||
else if (key === 'first' && (isArray(obj) || isString(obj))) return readFirst(obj)
|
||||
else if (key === 'last' && (isArray(obj) || isString(obj))) return readLast(obj)
|
||||
return value
|
||||
}
|
||||
}
|
||||
@@ -141,22 +141,16 @@ export function readJSProperty (obj: Scope, key: PropertyKey, ownPropertyOnly: b
|
||||
return obj[key]
|
||||
}
|
||||
|
||||
function readFirst (obj: Scope, ownPropertyOnly: boolean) {
|
||||
function readFirst (obj: Scope) {
|
||||
if (isArray(obj)) return obj[0]
|
||||
if (ownPropertyOnly && !hasOwnProperty.call(obj, 'first') && !(obj instanceof Drop)) return undefined
|
||||
return obj['first']
|
||||
return obj[0]
|
||||
}
|
||||
|
||||
function readLast (obj: Scope, ownPropertyOnly: boolean) {
|
||||
function readLast (obj: Scope) {
|
||||
if (isArray(obj)) return obj[obj.length - 1]
|
||||
if (ownPropertyOnly && !hasOwnProperty.call(obj, 'last') && !(obj instanceof Drop)) return undefined
|
||||
return obj['last']
|
||||
return obj[obj.length - 1]
|
||||
}
|
||||
|
||||
function readSize (obj: Scope, ownPropertyOnly: boolean) {
|
||||
if (hasOwnProperty.call(obj, 'size')) return obj['size']
|
||||
if (!ownPropertyOnly && obj['size'] !== undefined) return obj['size']
|
||||
if (isArray(obj) || isString(obj)) return obj.length
|
||||
if (obj instanceof Set || obj instanceof Map) return obj.size
|
||||
if (typeof obj === 'object') return Object.keys(obj).length
|
||||
function readSize (obj: Scope) {
|
||||
return obj.length
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user