Filter method

commit_hash:a7eb1850390d510f325348b64bc6b54a9f6109c7
This commit is contained in:
4eb0da
2025-12-01 23:27:32 +03:00
parent 2d4a1364e6
commit 72e6e68176
7 changed files with 439 additions and 30 deletions
@@ -13,3 +13,4 @@ export const URL = 'url';
export const DATETIME = 'datetime';
export const DICT = 'dict';
export const ARRAY = 'array';
export const FUNCTION = 'function';
+24 -7
View File
@@ -16,6 +16,8 @@ import { convertArgs, findBestMatchedFunc, type Func, funcByArgs, type FuncMatch
import {
checkIntegerOverflow,
evalError,
evalOuterError,
FuncError,
integerToNumber,
roundInteger,
typeToString,
@@ -492,35 +494,46 @@ function evalCallExpression(ctx: EvalContext, expr: CallExpression): EvalValue {
try {
return func.cb(ctx, ...args);
} catch (err: any) {
if (err && err instanceof FuncError) {
throw err;
}
const prefix = `${funcName}(${argsToStr(args)})`;
evalError(prefix, err.message);
}
}
function logFunctionMatchError(funcName: string, args: EvalValue[], findRes: FuncMatchError): never {
export function logFunctionMatchError(
funcName: string,
args: EvalValue[],
findRes: FuncMatchError,
isOuterFunc = false
): never {
const argsType = args.map(arg => typeToString(arg.type)).join(', ');
const prefix = `${funcName}(${argsToStr(args)})`;
const makeError: (msg: string, details: string) => never =
isOuterFunc ? evalOuterError : evalError;
if (findRes.type === 'few' && args.length === 0 && findRes.hasOverloads) {
evalError(prefix, 'Function requires non empty argument list.');
makeError(prefix, 'Function requires non empty argument list.');
} else if (findRes.type === 'many' || findRes.type === 'few' || findRes.type === 'mismatch') {
if (findRes.hasOverloads) {
evalError(prefix, `Function has no matching overload for given argument types: ${argsType}.`);
makeError(prefix, `Function has no matching overload for given argument types: ${argsType}.`);
} else {
// eslint-disable-next-line no-lonely-if
if (findRes.type === 'many' || findRes.type === 'few') {
if (findRes.def.args.some(arg => typeof arg === 'object' && arg.isVararg)) {
evalError(prefix, `At least ${findRes.def.args.length} argument(s) expected.`);
makeError(prefix, `At least ${findRes.def.args.length} argument(s) expected.`);
} else {
evalError(prefix, `Exactly ${findRes.def.args.length} argument(s) expected.`);
makeError(prefix, `Exactly ${findRes.def.args.length} argument(s) expected.`);
}
} else {
const expectedArgs = findRes.def.args.map(arg => typeToString(typeof arg === 'string' ? arg : arg.type)).join(', ');
evalError(prefix, `Invalid argument type: expected ${expectedArgs}, got ${argsType}.`);
makeError(prefix, `Invalid argument type: expected ${expectedArgs}, got ${argsType}.`);
}
}
} else {
evalError(prefix, `Unknown function name: ${funcName}.`);
makeError(prefix, `Unknown function name: ${funcName}.`);
}
}
@@ -564,6 +577,10 @@ function evalMethodExpression(ctx: EvalContext, expr: MethodExpression): EvalVal
try {
return func.cb(ctx, ...args);
} catch (err: any) {
if (err && err instanceof FuncError) {
throw err;
}
const prefix = `${methodName}(${argsToStr(args.slice(1))})`;
evalError(prefix, err.message);
}
@@ -1,8 +1,9 @@
import { parseColor } from '../../utils/correctColor';
import { toBigInt } from '../bigint';
import { ARRAY, BOOLEAN, COLOR, DICT, INTEGER, NUMBER, STRING, URL } from '../const';
import type { ArrayValue, BooleanValue, ColorValue, EvalContext, EvalTypes, EvalValue, IntegerValue, NumberValue, StringValue, UrlValue } from '../eval';
import { checkIntegerOverflow, checkUrl, transformColorValue, typeToString } from '../utils';
import { registerFunc, registerMethod } from './funcs';
import { ARRAY, BOOLEAN, COLOR, DICT, FUNCTION, INTEGER, NUMBER, STRING, URL } from '../const';
import { logFunctionMatchError, type ArrayValue, type BooleanValue, type ColorValue, type EvalContext, type EvalTypes, type EvalValue, type FuncValue, type IntegerValue, type NumberValue, type StringValue, type UrlValue } from '../eval';
import { checkIntegerOverflow, checkUrl, convertJsValueToDivKit, safeCheckUrl, transformColorValue, typeToString } from '../utils';
import { findBestMatchedFuncList, registerFunc, registerMethod, type Func, type FuncMatch } from './funcs';
function arrayGetter(jsType: string, runtimeType: string) {
return (ctx: EvalContext, array: ArrayValue, index: IntegerValue): EvalValue => {
@@ -128,6 +129,111 @@ function isEmpty(_ctx: EvalContext, array: ArrayValue): EvalValue {
};
}
function filter(ctx: EvalContext, array: ArrayValue, fn: FuncValue): EvalValue {
if (!array.value.length) {
return {
type: ARRAY,
value: []
};
}
return {
type: ARRAY,
value: array.value.filter(it => {
const argMatchers: EvalValue[][] = [];
if (typeof it === 'string') {
if (parseColor(it)) {
argMatchers.push([{
type: COLOR,
value: it
}]);
}
if (safeCheckUrl(it)) {
argMatchers.push([{
type: URL,
value: it
}]);
}
argMatchers.push([{
type: STRING,
value: it
}]);
} else if (typeof it === 'number') {
if (Math.round(it) === it) {
checkIntegerOverflow(ctx, it);
argMatchers.push([{
type: INTEGER,
value: toBigInt(it)
}]);
}
argMatchers.push([{
type: NUMBER,
value: it
}]);
} else if (typeof it === 'bigint') {
checkIntegerOverflow(ctx, it);
argMatchers.push([{
type: INTEGER,
value: it
}]);
} else if (Array.isArray(it)) {
argMatchers.push([{
type: ARRAY,
value: it
}]);
} else if (typeof it === 'object') {
if (it === null) {
throw new Error('Incorrect value type: Null');
}
argMatchers.push([{
type: DICT,
value: it
}]);
} else if (typeof it === 'boolean') {
argMatchers.push([{
type: BOOLEAN,
value: it ? 1 : 0
}]);
} else {
throw new Error(`Incorrect value type: ${typeToString(typeof it)}`);
}
let fnMatch: FuncMatch = {
type: 'missing'
};
for (const matchItem of argMatchers) {
fnMatch = findBestMatchedFuncList(fn.value, matchItem);
if ('func' in fnMatch) {
break;
}
}
let selectedFn: Func;
if ('func' in fnMatch) {
selectedFn = fnMatch.func;
} else {
const selectedFn = fn.value[0];
logFunctionMatchError(selectedFn.name || 'Function', argMatchers[0], fnMatch, true);
}
const argType = selectedFn.args[0];
const value = convertJsValueToDivKit(
ctx,
it,
typeof argType === 'string' ? argType : argType.type
);
const res = selectedFn.cb(ctx, value);
if (res.type !== BOOLEAN) {
throw new Error('Function must return boolean value.');
}
return res.value;
})
};
}
export function registerArray(): void {
registerFunc('getArrayString', [
ARRAY,
@@ -302,4 +408,5 @@ export function registerArray(): void {
registerMethod('getArray', [ARRAY, INTEGER], getArrayArray);
registerMethod('getDict', [ARRAY, INTEGER], getArrayDict);
registerMethod('isEmpty', [ARRAY], isEmpty);
registerMethod('filter', [ARRAY, FUNCTION], filter);
}
@@ -259,8 +259,7 @@ function matchFuncArgs(func: Func, args: EvalValue[], hasOverloads: boolean): {
};
}
export function findBestMatchedFunc(map: Map<string, Func[]>, funcName: string, args: EvalValue[]): FuncMatch {
const list = map.get(funcName);
export function findBestMatchedFuncList(list: Func[] | undefined, args: EvalValue[]): FuncMatch {
if (!list) {
return {
type: 'missing'
@@ -298,6 +297,10 @@ export function findBestMatchedFunc(map: Map<string, Func[]>, funcName: string,
return bestFunc;
}
export function findBestMatchedFunc(map: Map<string, Func[]>, funcName: string, args: EvalValue[]): FuncMatch {
return findBestMatchedFuncList(map.get(funcName), args);
}
export function convertArgs(func: Func, args: EvalValue[]): EvalValue[] {
return args.map((arg, i) => {
let funcArg = i >= func.args.length ? func.args[func.args.length - 1] : func.args[i];
+21 -4
View File
@@ -1,4 +1,4 @@
import type { EvalContext, EvalTypesWithoutDatetime, EvalValue, EvalValueBase, IntegerValue, NumberValue } from './eval';
import type { EvalContext, EvalTypes, EvalTypesWithoutDatetime, EvalValue, EvalValueBase, IntegerValue, NumberValue } from './eval';
import type { Node, Variable } from './ast';
import type { VariablesMap } from './eval';
import { walk } from './walk';
@@ -9,6 +9,9 @@ import { BOOLEAN, NUMBER } from './const';
import type { TypedValue } from '../../typings/common';
import type { MaybeMissing } from './json';
export class FuncError extends Error {
}
export function valToInternal(val: EvalValue): EvalValue {
if (val.type === 'url' || val.type === 'color') {
return {
@@ -112,6 +115,15 @@ export function checkUrl(val: unknown): void {
}
}
export function safeCheckUrl(val: unknown): boolean {
try {
checkUrl(val);
return true;
} catch {
return false;
}
}
export function gatherVarsFromAst(ast: Node): string[] {
const res = new Set<string>();
@@ -125,7 +137,11 @@ export function gatherVarsFromAst(ast: Node): string[] {
}
export function evalError(msg: string, details: string): never {
throw new Error(`Failed to evaluate [${msg}]. ${details}`);
throw new FuncError(`Failed to evaluate [${msg}]. ${details}`);
}
export function evalOuterError(_msg: string, details: string): never {
throw new Error(details);
}
export function containsUnsetVariables(ast: Node, variables: VariablesMap): boolean {
@@ -181,12 +197,13 @@ const EVAL_TYPE_TO_JS_TYPE = {
color: 'string',
url: 'string',
array: 'array',
dict: 'object'
dict: 'object',
datetime: 'never'
};
export function convertJsValueToDivKit(
ctx: EvalContext | undefined,
val: unknown,
evalType: EvalTypesWithoutDatetime
evalType: EvalTypes
): EvalValue {
if (evalType === 'function') {
throw new Error('Cannot convert function');
+3 -1
View File
@@ -268,7 +268,9 @@
}
],
"return_type": "array",
"platforms": []
"platforms": [
"web"
]
}
]
}
@@ -40,7 +40,9 @@
]
}
],
"platforms": []
"platforms": [
"web"
]
},
{
"expression": "@{source.filter(equalsOne)}",
@@ -74,7 +76,9 @@
]
}
],
"platforms": []
"platforms": [
"web"
]
},
{
"expression": "@{source.filter(lessThanThree)}",
@@ -109,7 +113,9 @@
]
}
],
"platforms": []
"platforms": [
"web"
]
},
{
"expression": "@{source.filter(alwaysFalse)}",
@@ -141,7 +147,9 @@
]
}
],
"platforms": []
"platforms": [
"web"
]
},
{
"expression": "@{boolArray.filter(isTrue)}",
@@ -177,13 +185,15 @@
]
}
],
"platforms": []
"platforms": [
"web"
]
},
{
"expression": "@{source.filter(nonExistentFunction)}",
"expected": {
"type": "error",
"value": "Failed to evaluate [filter(nonExistentFunction)]. Unknown function name: nonExistentFunction."
"value": "Variable 'nonExistentFunction' is missing."
},
"variables": [
{
@@ -196,7 +206,9 @@
}
],
"functions": [],
"platforms": []
"platforms": [
"web"
]
},
{
"expression": "@{source.filter(wrongReturnType)}",
@@ -227,13 +239,15 @@
]
}
],
"platforms": []
"platforms": [
"web"
]
},
{
"expression": "@{source.filter(containsQuery)}",
"expected": {
"type": "error",
"value": "Failed to evaluate [containsQuery]. Exactly 2 argument(s) expected."
"value": "Failed to evaluate [filter(containsQuery)]. Exactly 2 argument(s) expected."
},
"variables": [
{
@@ -263,13 +277,15 @@
]
}
],
"platforms": []
"platforms": [
"web"
]
},
{
"expression": "@{heterogeneousArray.filter(isStringOne)}",
"expected": {
"type": "error",
"value": "Failed to evaluate [filter(isStringOne)]. Incorrect value type: expected String, got Integer."
"value": "Failed to evaluate [filter(isStringOne)]. Invalid argument type: expected String, got Integer."
},
"variables": [
{
@@ -295,7 +311,253 @@
]
}
],
"platforms": []
"platforms": [
"web"
]
},
{
"expression": "@{emptyArray.filter(anyFunc)}",
"expected": {
"type": "array",
"value": []
},
"variables": [
{
"name": "emptyArray",
"type": "array",
"value": []
}
],
"functions": [
{
"name": "anyFunc",
"body": "@{true}",
"return_type": "boolean",
"arguments": []
}
],
"platforms": [
"web"
]
},
{
"expression": "@{integerArray.filter(manyFunc)}",
"expected": {
"type": "array",
"value": [
]
},
"variables": [
{
"name": "integerArray",
"type": "array",
"value": [
20,
40
]
}
],
"functions": [
{
"name": "manyFunc",
"body": "@{val > 10.0}",
"return_type": "boolean",
"arguments": [
{
"name": "val",
"type": "number"
}
]
},
{
"name": "manyFunc",
"body": "@{val < 10}",
"return_type": "boolean",
"arguments": [
{
"name": "val",
"type": "integer"
}
]
}
],
"platforms": [
"web"
]
},
{
"expression": "@{mixedArray.filter(manyNumberFunc)}",
"expected": {
"type": "array",
"value": [
40.5
]
},
"variables": [
{
"name": "mixedArray",
"type": "array",
"value": [
20,
40.5
]
}
],
"functions": [
{
"name": "manyNumberFunc",
"body": "@{val > 10.0}",
"return_type": "boolean",
"arguments": [
{
"name": "val",
"type": "number"
}
]
},
{
"name": "manyNumberFunc",
"body": "@{val < 10}",
"return_type": "boolean",
"arguments": [
{
"name": "val",
"type": "integer"
}
]
}
],
"platforms": [
"web"
]
},
{
"expression": "@{mixedColorArray.filter(manyColorFunc)}",
"expected": {
"type": "array",
"value": [
"#f00",
"blue"
]
},
"variables": [
{
"name": "mixedColorArray",
"type": "array",
"value": [
"#f00",
"blue"
]
}
],
"functions": [
{
"name": "manyColorFunc",
"body": "@{val == toColor('#FFFF0000')}",
"return_type": "boolean",
"arguments": [
{
"name": "val",
"type": "color"
}
]
},
{
"name": "manyColorFunc",
"body": "@{val == 'blue'}",
"return_type": "boolean",
"arguments": [
{
"name": "val",
"type": "string"
}
]
}
],
"platforms": [
"web"
]
},
{
"expression": "@{mixedUrlArray.filter(manyUrlFunc)}",
"expected": {
"type": "array",
"value": [
"https://divkit.tech",
"str"
]
},
"variables": [
{
"name": "mixedUrlArray",
"type": "array",
"value": [
"https://divkit.tech",
"str"
]
}
],
"functions": [
{
"name": "manyUrlFunc",
"body": "@{val == toUrl('https://divkit.tech')}",
"return_type": "boolean",
"arguments": [
{
"name": "val",
"type": "url"
}
]
},
{
"name": "manyUrlFunc",
"body": "@{val == 'str'}",
"return_type": "boolean",
"arguments": [
{
"name": "val",
"type": "string"
}
]
}
],
"platforms": [
"web"
]
},
{
"expression": "@{integerArray.filter(datetimeFunc)}",
"expected": {
"type": "error",
"value": "Failed to evaluate [filter(datetimeFunc)]. Invalid argument type: expected DateTime, got Integer."
},
"variables": [
{
"name": "integerArray",
"type": "array",
"value": [
1,
2,
3
]
}
],
"functions": [
{
"name": "datetimeFunc",
"body": "@{true}",
"return_type": "boolean",
"arguments": [
{
"name": "val",
"type": "datetime"
}
]
}
],
"platforms": [
"web"
]
}
]
}