mirror of
https://github.com/divkit/divkit.git
synced 2026-05-07 20:02:32 +00:00
Filter method
commit_hash:a7eb1850390d510f325348b64bc6b54a9f6109c7
This commit is contained in:
@@ -13,3 +13,4 @@ export const URL = 'url';
|
||||
export const DATETIME = 'datetime';
|
||||
export const DICT = 'dict';
|
||||
export const ARRAY = 'array';
|
||||
export const FUNCTION = 'function';
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user