import { NFError } from "./error"; /** * 条件表达式 AST */ type ExprNode = | { type: 'literal'; value: any } | { type: 'ident'; name: string } | { type: 'array'; elements: ExprNode[] } | { type: 'member'; object: ExprNode; property: ExprNode } | { type: 'call'; callee: ExprNode; args: ExprNode[] } | { type: 'unary'; op: '!' | '-'; argument: ExprNode } | { type: 'binary'; op: string; left: ExprNode; right: ExprNode }; interface Token{ type:string; value?:any; index:number; } /** 允许在 cond 中使用的全局对象(不含 Function / eval) */ const SAFE_GLOBALS:Record = { Math, Number, String, Boolean, Array, Date, JSON, Object, parseInt, parseFloat, isNaN, isFinite, Infinity, NaN }; const TWO_CHAR = new Set(['==', '!=', '<=', '>=', '&&', '||']); const KEYWORDS:Record = { true: { type: 'literal', value: true }, false: { type: 'literal', value: false }, null: { type: 'literal', value: null }, undefined: { type: 'literal', value: undefined }, in: { type: 'in', value: 'in' } }; /** * 流程条件表达式 * * 用于 sequence.cond,对流程变量求值。支持: * 比较 / 逻辑 / 算术 / 括号 / 属性与下标 / 数组字面量 / in / * 模型方法与白名单全局函数(Math、Number 等) */ export class NFExpression { /** * 求值函数(空表达式为 undefined,兼容旧测试) */ execFunc: Function; /** * 最近一次 val() 的结果 */ value:any; /** * 源串 */ readonly source:string; private ast:ExprNode; /** * 解析条件表达式。空串视为恒未求值(`val` 返回 `undefined`)。 * * @param exprStr - 条件表达式,如 `data==1 && amount>0` * @throws `1500` 当语法非法 */ constructor(exprStr: string) { this.source = (exprStr || '').trim(); if (!this.source) { return; } this.ast = parse(this.source); this.execFunc = (model:any) => evaluate(this.ast, model || {}); } /** * 对流程变量求值。语法错误在构造时抛出;求值异常返回 `undefined`。 * * @param model - 流程变量对象 * @returns 表达式结果;空表达式或求值失败时为 `undefined` */ public val(model?:any) { if (!this.ast) { this.value = undefined; return undefined; } try { this.value = evaluate(this.ast, model || {}); } catch (e) { this.value = undefined; } return this.value; } } /** 标识符起始字符。 */ function isIdentStart(c:string):boolean{ return /[\p{ID_Start}_$]/u.test(c); } /** 标识符后续字符。 */ function isIdentPart(c:string):boolean{ return /[\p{ID_Continue}_$]/u.test(c); } /** 十进制数字。 */ function isDigit(c:string):boolean{ return c >= '0' && c <= '9'; } /** * 将源串拆为 token。 * * @param src - 表达式源串 * @returns token 数组,末尾含 `eof` * @throws `1500` 当出现非法字符或赋值号 `=` */ function tokenize(src:string):Token[]{ const tokens:Token[] = []; let i = 0; const n = src.length; const push = (type:string, index:number, value?:any) => { tokens.push(value !== undefined ? { type, value, index } : { type, index }); }; while(i < n){ const c = src[i]; if(c === ' ' || c === '\t' || c === '\n' || c === '\r'){ i++; continue; } if(src.startsWith('===', i) || src.startsWith('!==', i)){ push(src.slice(i, i + 3), i); i += 3; continue; } const two = src.slice(i, i + 2); if(TWO_CHAR.has(two)){ push(two, i); i += 2; continue; } if(c === '='){ throw new NFError('1500', [`unexpected '=' at ${i}, use '=='`]); } if(c === "'" || c === '"'){ const { value, next } = readString(src, i); push('string', i, value); i = next; continue; } if(isDigit(c) || (c === '.' && isDigit(src[i + 1] || ''))){ const { value, next } = readNumber(src, i); push('number', i, value); i = next; continue; } if(isIdentStart(c)){ let j = i + 1; while(j < n && isIdentPart(src[j])){ j++; } const name = src.slice(i, j); const kw = KEYWORDS[name]; if(kw){ push(kw.type, i, kw.value); }else{ push('ident', i, name); } i = j; continue; } if('()[].,+-*/%!<>'.includes(c)){ push(c, i); i++; continue; } throw new NFError('1500', [`unexpected '${c}' at ${i}`]); } push('eof', i); return tokens; } /** * 读取字符串字面量。 * * @param src - 源串 * @param start - 起始引号下标 * @returns 字符串值与下一扫描位置 * @throws `1500` 当字符串未闭合 */ function readString(src:string, start:number):{ value:string; next:number }{ const quote = src[start]; let i = start + 1; let out = ''; while(i < src.length){ const c = src[i]; if(c === quote){ return { value: out, next: i + 1 }; } if(c === '\\'){ const n = src[i + 1]; if(n === undefined){ break; } const map:Record = { n: '\n', r: '\r', t: '\t', '\\': '\\', "'": "'", '"': '"' }; out += map[n] !== undefined ? map[n] : n; i += 2; continue; } out += c; i++; } throw new NFError('1500', [`unterminated string at ${start}`]); } /** * 读取数字字面量。 * * @param src - 源串 * @param start - 起始下标 * @returns 数值与下一扫描位置 * @throws `1500` 当数字非法 */ function readNumber(src:string, start:number):{ value:number; next:number }{ let i = start; while(i < src.length && isDigit(src[i])){ i++; } if(src[i] === '.'){ i++; while(i < src.length && isDigit(src[i])){ i++; } } if(src[i] === 'e' || src[i] === 'E'){ i++; if(src[i] === '+' || src[i] === '-'){ i++; } if(!isDigit(src[i] || '')){ throw new NFError('1500', [`invalid number at ${start}`]); } while(i < src.length && isDigit(src[i])){ i++; } } const value = Number(src.slice(start, i)); if(Number.isNaN(value)){ throw new NFError('1500', [`invalid number at ${start}`]); } return { value, next: i }; } /** * 递归下降解析表达式为 AST。 * * @param src - 表达式源串 * @returns 根节点 * @throws `1500` 当语法非法 */ function parse(src:string):ExprNode{ const tokens = tokenize(src); let p = 0; const peek = () => tokens[p]; const eat = (type?:string) => { const t = tokens[p]; if(type && t.type !== type){ throw new NFError('1500', [`expected '${type}' at ${t.index}`]); } p++; return t; }; const parseOr = ():ExprNode => { let left = parseAnd(); while(peek().type === '||'){ eat(); left = { type: 'binary', op: '||', left, right: parseAnd() }; } return left; }; const parseAnd = ():ExprNode => { let left = parseEquality(); while(peek().type === '&&'){ eat(); left = { type: 'binary', op: '&&', left, right: parseEquality() }; } return left; }; const parseEquality = ():ExprNode => { let left = parseRel(); while(['==', '!=', '===', '!=='].includes(peek().type)){ const op = eat().type; left = { type: 'binary', op, left, right: parseRel() }; } return left; }; const parseRel = ():ExprNode => { let left = parseAdd(); while(['<', '>', '<=', '>=', 'in'].includes(peek().type)){ const op = eat().type; left = { type: 'binary', op, left, right: parseAdd() }; } return left; }; const parseAdd = ():ExprNode => { let left = parseMul(); while(peek().type === '+' || peek().type === '-'){ const op = eat().type; left = { type: 'binary', op, left, right: parseMul() }; } return left; }; const parseMul = ():ExprNode => { let left = parseUnary(); while(peek().type === '*' || peek().type === '/' || peek().type === '%'){ const op = eat().type; left = { type: 'binary', op, left, right: parseUnary() }; } return left; }; const parseUnary = ():ExprNode => { if(peek().type === '!' || peek().type === '-'){ const op = eat().type as '!' | '-'; return { type: 'unary', op, argument: parseUnary() }; } return parsePostfix(); }; const parsePostfix = ():ExprNode => { let node = parsePrimary(); for(;;){ if(peek().type === '.'){ eat(); const name = eat('ident'); node = { type: 'member', object: node, property: { type: 'literal', value: name.value } }; }else if(peek().type === '['){ eat(); const property = parseOr(); eat(']'); node = { type: 'member', object: node, property }; }else if(peek().type === '('){ eat(); const args:ExprNode[] = []; if(peek().type !== ')'){ args.push(parseOr()); while(peek().type === ','){ eat(); args.push(parseOr()); } } eat(')'); node = { type: 'call', callee: node, args }; }else{ break; } } return node; }; const parsePrimary = ():ExprNode => { const t = peek(); if(t.type === 'number' || t.type === 'string' || t.type === 'literal'){ eat(); return { type: 'literal', value: t.value }; } if(t.type === 'ident'){ eat(); return { type: 'ident', name: t.value }; } if(t.type === '('){ eat(); const inner = parseOr(); eat(')'); return inner; } if(t.type === '['){ eat(); const elements:ExprNode[] = []; if(peek().type !== ']'){ elements.push(parseOr()); while(peek().type === ','){ eat(); elements.push(parseOr()); } } eat(']'); return { type: 'array', elements }; } throw new NFError('1500', [`unexpected '${t.type}' at ${t.index}`]); }; const ast = parseOr(); if(peek().type !== 'eof'){ throw new NFError('1500', [`unexpected '${peek().type}' at ${peek().index}`]); } return ast; } /** 判断对象自身是否含有指定键。 */ function hasOwn(obj:any, key:string):boolean{ return obj != null && Object.prototype.hasOwnProperty.call(obj, key); } /** * 解析标识符:优先流程变量,其次白名单全局(`Math` 等)。 * * @param name - 标识符 * @param model - 流程变量 * @returns 对应值;未找到时为 `undefined` */ function resolveIdent(name:string, model:any):any{ if(name === '$model'){ return model; } if(hasOwn(model, name)){ return model[name]; } if(hasOwn(SAFE_GLOBALS, name)){ return SAFE_GLOBALS[name]; } return undefined; } /** * 求值 AST。 * * @param node - 表达式节点 * @param model - 流程变量 * @returns 求值结果 */ function evaluate(node:ExprNode, model:any):any{ switch(node.type){ case 'literal': return node.value; case 'ident': return resolveIdent(node.name, model); case 'array': return node.elements.map(el => evaluate(el, model)); case 'member': { const obj = evaluate(node.object, model); if(obj == null){ return undefined; } const key = evaluate(node.property, model); return obj[key]; } case 'call': return evalCall(node, model); case 'unary': { const v = evaluate(node.argument, model); return node.op === '!' ? !v : -v; } case 'binary': return evalBinary(node, model); default: return undefined; } } /** * 求值函数调用(支持成员方法与白名单全局函数)。 * * @param node - 调用节点 * @param model - 流程变量 * @returns 调用结果;被调方不是函数时为 `undefined` */ function evalCall(node:Extract, model:any):any{ const args = node.args.map(a => evaluate(a, model)); const callee = node.callee; if(callee.type === 'member'){ const obj = evaluate(callee.object, model); if(obj == null){ return undefined; } const key = evaluate(callee.property, model); const fn = obj[key]; if(typeof fn !== 'function'){ return undefined; } return fn.apply(obj, args); } const fn = evaluate(callee, model); if(typeof fn !== 'function'){ return undefined; } return fn.apply(model, args); } /** * 求值二元运算(含短路 `&&` / `||` 与 `in`)。 * * @param node - 二元节点 * @param model - 流程变量 * @returns 运算结果 */ function evalBinary(node:Extract, model:any):any{ const op = node.op; if(op === '||'){ const left = evaluate(node.left, model); return left ? left : evaluate(node.right, model); } if(op === '&&'){ const left = evaluate(node.left, model); return left ? evaluate(node.right, model) : left; } const left = evaluate(node.left, model); const right = evaluate(node.right, model); switch(op){ case '==': return left == right; case '!=': return left != right; case '===': return left === right; case '!==': return left !== right; case '<': return left < right; case '>': return left > right; case '<=': return left <= right; case '>=': return left >= right; case '+': return left + right; case '-': return left - right; case '*': return left * right; case '/': return left / right; case '%': return left % right; case 'in': if(Array.isArray(right)){ return right.includes(left); } if(right != null && typeof right === 'object'){ return left in right; } return false; default: return undefined; } }