diff --git a/src/context/context.spec.ts b/src/context/context.spec.ts index b7e182e5b..f122174c9 100644 --- a/src/context/context.spec.ts +++ b/src/context/context.spec.ts @@ -138,11 +138,6 @@ describe('Context', function () { ctx.push({ foo: [] }) return expect(ctx.getSync(['foo', 'reduce'])).toEqual(undefined) }) - it('should return undefined for typical Object.prototype properties (e.g. constructor, valueOf)', function () { - ctx.push({ obj: {} }) - expect(ctx.getSync(['obj', 'constructor'])).toEqual(undefined) - expect(ctx.getSync(['obj', 'valueOf'])).toEqual(undefined) - }) it('should return undefined for function prototype property', function () { function Foo () {} Foo.prototype.bar = 'BAR' diff --git a/src/context/context.ts b/src/context/context.ts index a9a0083c5..205a57622 100644 --- a/src/context/context.ts +++ b/src/context/context.ts @@ -2,7 +2,7 @@ import { getPerformance } from '../util/performance' import { Drop } from '../drop/drop' import { __assign } from 'tslib' import { NormalizedFullOptions, defaultOptions, RenderOptions } from '../liquid-options' -import { Scope, createScope } from './scope' +import { Scope } from './scope' import { hasOwnProperty, isArray, isNil, isUndefined, isString, isFunction, toLiquid, InternalUndefinedVariableError, toValueSync, isObject, Limiter, toValue } from '../util' type PropertyKey = string | number; @@ -12,7 +12,7 @@ export class Context { * insert a Context-level empty scope, * for tags like `{% capture %}` `{% assign %}` to operate */ - private scopes: Scope[] = [createScope()] + private scopes: Scope[] = [{}] private registers = {} /** * user passed in scope @@ -62,7 +62,7 @@ export class Context { } public getAll () { return [this.globals, this.environments, ...this.scopes] - .reduce((ctx, val) => __assign(ctx, val), createScope()) + .reduce((ctx, val) => __assign(ctx, val), {}) } /** * @deprecated use `_get()` or `getSync()` instead @@ -102,7 +102,7 @@ export class Context { public bottom () { return this.scopes[0] } - public spawn (scope: object = createScope()) { + public spawn (scope = {}) { return new Context(scope, this.opts, { sync: this.sync, globals: this.globals, diff --git a/src/context/scope.ts b/src/context/scope.ts index 7d21a5f2d..9fcc06dae 100644 --- a/src/context/scope.ts +++ b/src/context/scope.ts @@ -5,13 +5,3 @@ interface ScopeObject extends Record { } export type Scope = ScopeObject | Drop - -/** - * Plain scope bag with a null prototype so lookups like `__proto__` are not the - * Object.prototype accessor unless explicitly assigned as an own property. - */ -export function createScope (props?: Record): ScopeObject { - return props == null - ? Object.create(null) - : Object.assign(Object.create(null), props) -} diff --git a/src/filters/array.ts b/src/filters/array.ts index 640aea7d6..f22fcce20 100644 --- a/src/filters/array.ts +++ b/src/filters/array.ts @@ -2,7 +2,7 @@ import { toArray, argumentsToValue, toValue, stringify, caseInsensitiveCompare, import { arrayIncludes, equals, evalToken, isTruthy } from '../render' import { Value, FilterImpl } from '../template' import { Tokenizer } from '../parser' -import { createScope, type Scope } from '../context' +import type { Scope } from '../context' import { EmptyDrop } from '../drop' export const join = argumentsToValue(function (this: FilterImpl, v: any[], arg: string) { @@ -139,7 +139,7 @@ function * filter_exp (this: FilterImpl, include: boolean, arr const array = toArray(arr) this.context.memoryLimit.use(array.length) for (const item of array) { - this.context.push(createScope({ [itemName]: item })) + this.context.push({ [itemName]: item }) const value = yield keyTemplate.value(this.context) this.context.pop() if (value === include) filtered.push(item) @@ -182,7 +182,7 @@ export function * group_by_exp (this: FilterImpl, arr: T[], it arr = toEnumerable(arr) this.context.memoryLimit.use(arr.length) for (const item of arr) { - this.context.push(createScope({ [itemName]: item })) + this.context.push({ [itemName]: item }) const key = yield keyTemplate.value(this.context) this.context.pop() if (!map.has(key)) map.set(key, []) @@ -205,7 +205,7 @@ function * search_exp (this: FilterImpl, arr: T[], itemName: s const predicate = new Value(stringify(exp), this.liquid) const array = toArray(arr) for (let index = 0; index < array.length; index++) { - this.context.push(createScope({ [itemName]: array[index] })) + this.context.push({ [itemName]: array[index] }) const value = yield predicate.value(this.context) this.context.pop() if (value) return [index, array[index]] diff --git a/src/index.ts b/src/index.ts index 7e869552f..721c21b50 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,7 +7,7 @@ export { Drop } from './drop' export type { Comparable } from './drop' export { Emitter } from './emitters' export { defaultOperators, Operators, evalToken, evalQuotedToken, Expression, isFalsy, isTruthy } from './render' -export { Context, Scope, createScope } from './context' +export { Context, Scope } from './context' export { Value, Hash, Template, FilterImplOptions, Tag, Filter, Output, Variable, VariableLocation, VariableSegments, Variables, StaticAnalysis, StaticAnalysisOptions, analyze, analyzeSync, Arguments, PartialScope } from './template' export type { TagRenderReturn } from './template' export { Token, TopLevelToken, TagToken, ValueToken } from './tokens' diff --git a/src/tags/block.ts b/src/tags/block.ts index 56dca80af..947f0b3e5 100644 --- a/src/tags/block.ts +++ b/src/tags/block.ts @@ -1,4 +1,4 @@ -import { BlockMode, createScope } from '../context' +import { BlockMode } from '../context' import { isTagToken } from '../util' import { BlockDrop } from '../drop' import { Liquid, TagToken, TopLevelToken, Template, Context, Emitter, Tag } from '..' @@ -38,7 +38,7 @@ export default class extends Tag { if (stack.includes(self)) throw new Error('block tag cannot be nested') stack.push(self) - ctx.push(createScope({ block: superBlock })) + ctx.push({ block: superBlock }) yield liquid.renderer.renderTemplates(templates, ctx, emitter) ctx.pop() stack.pop() diff --git a/src/tags/for.ts b/src/tags/for.ts index 775229f5b..0d29dee78 100644 --- a/src/tags/for.ts +++ b/src/tags/for.ts @@ -1,4 +1,4 @@ -import { Hash, ValueToken, Liquid, Tag, evalToken, Emitter, TagToken, TopLevelToken, Context, Template, ParseStream, createScope } from '..' +import { Hash, ValueToken, Liquid, Tag, evalToken, Emitter, TagToken, TopLevelToken, Context, Template, ParseStream } from '..' import { assertEmpty, isValueToken, toEnumerable } from '../util' import { ForloopDrop } from '../drop/forloop-drop' import { Parser } from '../parser' @@ -50,7 +50,7 @@ export default class extends Tag { } const continueKey = 'continue-' + this.variable + '-' + this.collection.getText() - ctx.push(createScope({ continue: ctx.getRegister(continueKey, 0) })) + ctx.push({ continue: ctx.getRegister(continueKey, {}) }) const hash = yield this.hash.render(ctx) ctx.pop() @@ -65,7 +65,7 @@ export default class extends Tag { }, collection) ctx.setRegister(continueKey, (hash['offset'] || 0) + collection.length) - const scope = createScope({ forloop: new ForloopDrop(collection.length, this.collection.getText(), this.variable) }) + const scope = { forloop: new ForloopDrop(collection.length, this.collection.getText(), this.variable) } ctx.push(scope) for (const item of collection) { scope[this.variable] = item diff --git a/src/tags/include.ts b/src/tags/include.ts index 74b404bbe..3ad785b90 100644 --- a/src/tags/include.ts +++ b/src/tags/include.ts @@ -1,4 +1,4 @@ -import { Template, ValueToken, TopLevelToken, Liquid, Tag, assert, evalToken, Hash, Emitter, TagToken, Context, createScope } from '..' +import { Template, ValueToken, TopLevelToken, Liquid, Tag, assert, evalToken, Hash, Emitter, TagToken, Context } from '..' import { BlockMode, Scope } from '../context' import { Parser } from '../parser' import { Argument, Arguments, PartialScope } from '../template' @@ -37,7 +37,7 @@ export default class extends Tag { const scope = (yield hash.render(ctx)) as Scope if (withVar) scope[filepath] = yield evalToken(withVar, ctx) const templates = (yield liquid._parsePartialFile(filepath, ctx.sync, this['currentFile'])) as Template[] - ctx.push(ctx.opts.jekyllInclude ? createScope({ include: scope }) : Object.assign(createScope(), scope)) + ctx.push(ctx.opts.jekyllInclude ? { include: scope } : scope) yield renderer.renderTemplates(templates, ctx, emitter) ctx.pop() ctx.restoreRegister(saved) diff --git a/src/tags/layout.ts b/src/tags/layout.ts index ccd176d4b..cf1e7270f 100644 --- a/src/tags/layout.ts +++ b/src/tags/layout.ts @@ -1,4 +1,4 @@ -import { Scope, Template, Liquid, Tag, assert, Emitter, Hash, TagToken, TopLevelToken, Context, createScope } from '..' +import { Scope, Template, Liquid, Tag, assert, Emitter, Hash, TagToken, TopLevelToken, Context } from '..' import { BlockMode } from '../context' import { parseFilePath, renderFilePath, ParsedFileName } from './render' import { BlankDrop } from '../drop' @@ -39,7 +39,7 @@ export default class extends Tag { ctx.setRegister('blockMode', BlockMode.OUTPUT) // render the layout file use stored blocks - ctx.push(Object.assign(createScope(), (yield args.render(ctx)) as Scope)) + ctx.push((yield args.render(ctx)) as Scope) yield renderer.renderTemplates(templates, ctx, emitter) ctx.pop() } diff --git a/src/tags/tablerow.ts b/src/tags/tablerow.ts index c836b0d32..91f840445 100644 --- a/src/tags/tablerow.ts +++ b/src/tags/tablerow.ts @@ -1,5 +1,5 @@ import { isValueToken, toEnumerable } from '../util' -import { ValueToken, Liquid, Tag, evalToken, Emitter, Hash, TagToken, TopLevelToken, Context, Template, ParseStream, createScope } from '..' +import { ValueToken, Liquid, Tag, evalToken, Emitter, Hash, TagToken, TopLevelToken, Context, Template, ParseStream } from '..' import { TablerowloopDrop } from '../drop/tablerowloop-drop' import { Parser } from '../parser' import { Arguments } from '../template' @@ -48,7 +48,7 @@ export default class extends Tag { const r = this.liquid.renderer const tablerowloop = new TablerowloopDrop(collection.length, cols, this.collection.getText(), this.variable) - const scope = createScope({ tablerowloop }) + const scope = { tablerowloop } ctx.push(scope) for (let idx = 0; idx < collection.length; idx++, tablerowloop.next()) { diff --git a/test/e2e/issues.spec.ts b/test/e2e/issues.spec.ts index 2a4932985..c5243fe66 100644 --- a/test/e2e/issues.spec.ts +++ b/test/e2e/issues.spec.ts @@ -77,22 +77,6 @@ describe('Issues', function () { ) expect(html).toBe('BAR') }) - it('filter/tag maps are null-prototype (node + UMD)', async () => { - const nodeEngine = new Liquid() - const umdEngine = new LiquidUMD() - expect(Object.getPrototypeOf(nodeEngine.filters)).toBeNull() - expect(Object.getPrototypeOf(nodeEngine.tags)).toBeNull() - expect(Object.getPrototypeOf(umdEngine.filters)).toBeNull() - expect(Object.getPrototypeOf(umdEngine.tags)).toBeNull() - }) - it('filter/tag lookups ignore Object.prototype keys unless registered (node + UMD)', async () => { - for (const LiquidClass of [Liquid, LiquidUMD]) { - const engine = new LiquidClass() - await expect(engine.parseAndRender('{{ x | constructor }}', { x: 'OK' })).resolves.toBe('OK') - await expect(new LiquidClass({ strictFilters: true }).parseAndRender('{{ x | constructor }}', { x: 'OK' })).rejects.toThrow(/undefined filter/) - expect(() => engine.parse('{% constructor %}')).toThrow('tag "constructor" not found') - } - }) it('lenientIf not working as expected in umd #313', async () => { const engine = new LiquidUMD({ strictVariables: true, diff --git a/test/integration/liquid/register-filters.spec.ts b/test/integration/liquid/register-filters.spec.ts index e60b781ae..a0c6bcd64 100644 --- a/test/integration/liquid/register-filters.spec.ts +++ b/test/integration/liquid/register-filters.spec.ts @@ -61,19 +61,9 @@ describe('liquid#registerFilter()', function () { }) }) - describe('filter registry storage', () => { - it('should use a null-prototype map for filters', () => { - expect(Object.getPrototypeOf(liquid.filters)).toBeNull() - }) - it('should treat Object.prototype keys as unregistered unless explicitly registered', async () => { - const registered = new Set(Object.keys(liquid.filters)) - const strict = new Liquid({ strictFilters: true }) - for (const name of Object.getOwnPropertyNames(Object.prototype)) { - if (registered.has(name)) continue - const out = await liquid.parseAndRender(`{{ x | ${name} }}`, { x: 42 }) - expect(out).toBe('42') - await expect(strict.parseAndRender(`{{ 1 | ${name} }}`)).rejects.toThrow('undefined filter') - } - }) + it('should not treat Object.prototype names as registered filters', async () => { + expect(Object.getPrototypeOf(liquid.filters)).toBeNull() + await expect(liquid.parseAndRender('{{ x | constructor }}', { x: 42 })).resolves.toBe('42') + await expect(new Liquid({ strictFilters: true }).parseAndRender('{{ 1 | constructor }}')).rejects.toThrow('undefined filter') }) }) diff --git a/test/integration/liquid/register-tags.spec.ts b/test/integration/liquid/register-tags.spec.ts index b520a66da..76d6cdf4e 100644 --- a/test/integration/liquid/register-tags.spec.ts +++ b/test/integration/liquid/register-tags.spec.ts @@ -39,17 +39,9 @@ describe('liquid#registerTag()', function () { return expect(html).toBe('ABC') }) - describe('tag registry storage', () => { - it('should use a null-prototype map for tags', () => { - expect(Object.getPrototypeOf(new Liquid().tags)).toBeNull() - }) - it('should not resolve names that exist only on Object.prototype', () => { - const l = new Liquid() - const registered = new Set(Object.keys(l.tags)) - for (const name of Object.getOwnPropertyNames(Object.prototype)) { - if (registered.has(name)) continue - expect(() => l.parse(`{% ${name} %}`)).toThrow(`tag "${name}" not found`) - } - }) + it('should not treat Object.prototype names as registered tags', () => { + const l = new Liquid() + expect(Object.getPrototypeOf(l.tags)).toBeNull() + expect(() => l.parse('{% constructor %}')).toThrow('tag "constructor" not found') }) })