From 5c9532ada7c5b67ef81f863534d484480e678029 Mon Sep 17 00:00:00 2001 From: Yang Jun Date: Mon, 6 Jul 2026 23:51:42 +0800 Subject: [PATCH] fix(filters): charge json/inspect replacer by serialized node size Replace the flat 1-unit charge for non-string JSON nodes with per-type estimates (primitives via JSON.stringify length, containers by structure). Co-authored-by: Cursor --- src/filters/misc.ts | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/filters/misc.ts b/src/filters/misc.ts index a80dd6ece..447376550 100644 --- a/src/filters/misc.ts +++ b/src/filters/misc.ts @@ -2,6 +2,18 @@ import { isFalsy } from '../render/boolean' import { identify, isArray, isString, toValue } from '../util/underscore' import { FilterImpl } from '../template' +function chargeJsonReplacerValue (memoryLimit: { use(count: number): void }, val: unknown) { + if (typeof val === 'string') { + memoryLimit.use(val.length) + } else if (val === null || typeof val === 'number' || typeof val === 'boolean') { + memoryLimit.use(JSON.stringify(val).length) + } else if (Array.isArray(val)) { + memoryLimit.use(val.length + 1) + } else if (typeof val === 'object') { + memoryLimit.use(2) + } +} + function defaultFilter (this: FilterImpl, value: T1, defaultValue: T2, ...args: Array<[string, any]>): T1 | T2 { value = toValue(value) if (isArray(value) || isString(value)) return value.length ? value : defaultValue @@ -12,7 +24,7 @@ function defaultFilter (this: FilterImpl, value: T1, def function json (this: FilterImpl, value: any, space = 0) { const memoryLimit = this.context.memoryLimit return JSON.stringify(value, (_key, val) => { - memoryLimit.use(typeof val === 'string' ? val.length : 1) + chargeJsonReplacerValue(memoryLimit, val) return val }, space) } @@ -21,12 +33,18 @@ function inspect (this: FilterImpl, value: any, space = 0) { const memoryLimit = this.context.memoryLimit const ancestors: object[] = [] return JSON.stringify(value, function (this: unknown, _key: unknown, value: any) { - memoryLimit.use(typeof value === 'string' ? value.length : 1) - if (typeof value !== 'object' || value === null) return value + if (typeof value !== 'object' || value === null) { + chargeJsonReplacerValue(memoryLimit, value) + return value + } // `this` is the object that value is contained in, i.e., its direct parent. while (ancestors.length > 0 && ancestors[ancestors.length - 1] !== this) ancestors.pop() - if (ancestors.includes(value)) return '[Circular]' + if (ancestors.includes(value)) { + memoryLimit.use('[Circular]'.length) + return '[Circular]' + } ancestors.push(value) + chargeJsonReplacerValue(memoryLimit, value) return value }, space) }