From fec3dfc0bfeaaa6352daafd490597d057ab293ff Mon Sep 17 00:00:00 2001 From: 4eb0da <4eb0da@yandex-team.com> Date: Thu, 27 Nov 2025 01:43:39 +0300 Subject: [PATCH] Initial funcs support commit_hash:78f512cfc76f18e5db8b956eaa1703e4b23e07ca --- client/web/divkit/src/expressions/eval.ts | 21 +++++++++++++++---- .../src/expressions/funcs/customFuncs.ts | 4 ++++ .../web/divkit/src/expressions/funcs/funcs.ts | 1 + client/web/divkit/src/expressions/json.ts | 6 ++++++ client/web/divkit/src/expressions/utils.ts | 6 ++++++ .../tests/expressions/expressions.test.ts | 13 +++++++++++- .../custom_functions.json | 14 +++++++++---- 7 files changed, 56 insertions(+), 9 deletions(-) diff --git a/client/web/divkit/src/expressions/eval.ts b/client/web/divkit/src/expressions/eval.ts index 00ef4a041..183aefa75 100644 --- a/client/web/divkit/src/expressions/eval.ts +++ b/client/web/divkit/src/expressions/eval.ts @@ -32,9 +32,9 @@ import type { CustomFunctions } from './funcs/customFuncs'; export type VariablesMap = Map; -export type EvalTypes = 'string' | 'number' | 'integer' | 'boolean' | 'color' | 'url' | 'datetime' | 'dict' | 'array'; +export type EvalTypes = 'string' | 'number' | 'integer' | 'boolean' | 'color' | 'url' | 'datetime' | 'dict' | 'array' | 'function'; -export type EvalTypesWithoutDatetime = 'string' | 'number' | 'integer' | 'boolean' | 'color' | 'url' | 'dict' | 'array'; +export type EvalTypesWithoutDatetime = 'string' | 'number' | 'integer' | 'boolean' | 'color' | 'url' | 'dict' | 'array' | 'function'; export interface EvalValueBase { type: string; @@ -86,8 +86,13 @@ export interface ArrayValue extends EvalValueBase { value: unknown[]; } +export interface FuncValue extends EvalValueBase { + type: 'function'; + value: Func[]; +} + export type EvalValue = StringValue | UrlValue | ColorValue | NumberValue | IntegerValue | - BooleanValue | DatetimeValue | DictValue | ArrayValue; + BooleanValue | DatetimeValue | DictValue | ArrayValue | FuncValue; export interface EvalError { type: 'error'; @@ -463,7 +468,7 @@ function evalCallExpression(ctx: EvalContext, expr: CallExpression): EvalValue { const builtInFindRes = findBestMatchedFunc(funcs, funcName, args); // Assign errors only there is no match error in user defined funcs - if ('func' in builtInFindRes || !findRes) { + if ('func' in builtInFindRes || !findRes || findRes.type === 'missing') { findRes = builtInFindRes; } } @@ -566,6 +571,14 @@ function evalMethodExpression(ctx: EvalContext, expr: MethodExpression): EvalVal function evalVariable(ctx: EvalContext, expr: Variable): EvalValue { const varName = expr.id.name; + const customFuncs = ctx.customFunctions?.get(varName); + if (customFuncs) { + return { + type: 'function', + value: customFuncs + }; + } + const variable = ctx.variables.get(varName); if (variable) { diff --git a/client/web/divkit/src/expressions/funcs/customFuncs.ts b/client/web/divkit/src/expressions/funcs/customFuncs.ts index 1f40af921..731684c91 100644 --- a/client/web/divkit/src/expressions/funcs/customFuncs.ts +++ b/client/web/divkit/src/expressions/funcs/customFuncs.ts @@ -59,6 +59,7 @@ export function customFunctionWrap(fn: DivFunction): Func { let ast: Node | undefined; return { + name: fn.name, args: fn.arguments.map(it => { return { type: it.type @@ -74,6 +75,9 @@ export function customFunctionWrap(fn: DivFunction): Func { const vars: VariablesMap = new Map(); args.forEach((arg, index) => { + if (arg.type === 'function') { + throw new Error('Incorrect argument type: function'); + } const instance = createConstVariable(fn.arguments[index].name, arg.type, arg.value); // DatetimeVariable doesnt exist right know, but works fine vars.set(instance.getName(), instance as Variable); diff --git a/client/web/divkit/src/expressions/funcs/funcs.ts b/client/web/divkit/src/expressions/funcs/funcs.ts index b581e16cc..11b445749 100644 --- a/client/web/divkit/src/expressions/funcs/funcs.ts +++ b/client/web/divkit/src/expressions/funcs/funcs.ts @@ -9,6 +9,7 @@ export type FuncArg = EvalTypes | { } export interface Func { + name?: string; args: FuncArg[]; cb(ctx: EvalContext, ...args: EvalValue[]): EvalValue; } diff --git a/client/web/divkit/src/expressions/json.ts b/client/web/divkit/src/expressions/json.ts index 493ed5ba9..02e3a8ca3 100644 --- a/client/web/divkit/src/expressions/json.ts +++ b/client/web/divkit/src/expressions/json.ts @@ -105,6 +105,12 @@ class ExpressionBinding { usedVars: res.usedVars }; } + if (result.type === 'function') { + return { + result: `<${result.value[0]?.name || 'Function'}>` as T, + usedVars: res.usedVars + }; + } if (!keepComplex && (result.type === 'array' || result.type === 'dict')) { try { return { diff --git a/client/web/divkit/src/expressions/utils.ts b/client/web/divkit/src/expressions/utils.ts index 89293fe0a..84b3c1537 100644 --- a/client/web/divkit/src/expressions/utils.ts +++ b/client/web/divkit/src/expressions/utils.ts @@ -60,6 +60,8 @@ export function valToString(val: EvalValue, stringifyComplex: boolean): string { return ''; } else if (val.type === 'array') { return ''; + } else if (val.type === 'function') { + return val.value[0].name || 'Function'; } // For purpose when new eval value types will be added @@ -186,6 +188,10 @@ export function convertJsValueToDivKit( val: unknown, evalType: EvalTypesWithoutDatetime ): EvalValue { + if (evalType === 'function') { + throw new Error('Cannot convert function'); + } + const jsType = EVAL_TYPE_TO_JS_TYPE[evalType]; let type: string = typeof val; diff --git a/client/web/divkit/tests/expressions/expressions.test.ts b/client/web/divkit/tests/expressions/expressions.test.ts index 981f24af8..307eeb815 100644 --- a/client/web/divkit/tests/expressions/expressions.test.ts +++ b/client/web/divkit/tests/expressions/expressions.test.ts @@ -9,6 +9,8 @@ import { evalExpression, type EvalResult } from '../../src/expressions/eval'; import { transformColorValue, valToString } from '../../src/expressions/utils'; import { parse } from '../../src/expressions/expressions'; import { createVariable } from '../../src/expressions/variable'; +import { customFunctionWrap, type CustomFunctions } from '../../src/expressions/funcs/customFuncs'; +import type { DivFunction } from '../../typings/common'; const path = require('path'); const fs = require('fs'); @@ -46,6 +48,7 @@ function convertVals(val: EvalResult) { function runCase(item: any) { const vars = new Map(); + const customFunctions: CustomFunctions = new Map(); if (item.variables) { for (const variable of item.variables) { let value; @@ -89,6 +92,14 @@ function runCase(item: any) { vars.set(variable.name, createVariable(variable.name, variable.type, value)); } } + if (item.functions) { + for (const func of item.functions) { + const fn = func as DivFunction; + const list = customFunctions.get(fn.name) || []; + list.push(customFunctionWrap(fn)); + customFunctions.set(fn.name, list); + } + } let ast; try { ast = parse(item.expression, { @@ -105,7 +116,7 @@ function runCase(item: any) { } return; } - const res = evalExpression(vars, undefined, undefined, ast, { + const res = evalExpression(vars, customFunctions, undefined, ast, { weekStartDay: item.platform_specific?.web?.weekStartDay || 0 }); if (item.expected.value !== '' || res.result.type !== 'error') { diff --git a/test_data/expression_test_data/custom_functions.json b/test_data/expression_test_data/custom_functions.json index f12eab88c..bcf4c24d1 100644 --- a/test_data/expression_test_data/custom_functions.json +++ b/test_data/expression_test_data/custom_functions.json @@ -24,7 +24,8 @@ } ], "platforms": [ - "ios" + "ios", + "web" ] }, { @@ -51,7 +52,8 @@ } ], "platforms": [ - "ios" + "ios", + "web" ] }, { @@ -74,7 +76,9 @@ ] } ], - "platforms": [] + "platforms": [ + "web" + ] }, { "expression": "@{toString(myFunction)}", @@ -96,7 +100,9 @@ ] } ], - "platforms": [] + "platforms": [ + "web" + ] } ] }