/** * 静态可检 —— 编排画布「画图即校验」与安装静态审的依据(编码规范 §8.3;OS 03)。 * * 检三样:①语法能不能过 ②引用的函数在不在封闭集合里 ③引用了哪些路径(供必填字段核对)。 */ import type { Node, UnaryTest } from './ast.ts'; import { SfeelSyntaxError } from './lexer.ts'; import { parse, parseUnaryTest } from './parser.ts'; import { BUILTIN_FUNCTIONS } from './evaluate.ts'; export type StaticIssue = { readonly kind: 'syntax' | 'unknown-function'; readonly message: string; }; export type StaticCheckResult = { readonly ok: boolean; readonly issues: readonly StaticIssue[]; /** 表达式引用的全部根路径 —— 编排器据此核对上游载荷是否供得上 */ readonly references: readonly string[]; }; export function staticCheck(expression: string, mode: 'expression' | 'unary-test' = 'expression'): StaticCheckResult { let node: Node | undefined; const issues: StaticIssue[] = []; try { if (mode === 'unary-test') { const test = parseUnaryTest(expression); const nodes = collectTestNodes(test); const references = new Set(); for (const n of nodes) walk(n, issues, references); return { ok: issues.length === 0, issues, references: [...references] }; } node = parse(expression); } catch (error) { if (error instanceof SfeelSyntaxError) return { ok: false, issues: [{ kind: 'syntax', message: error.message }], references: [] }; throw error; } const references = new Set(); walk(node, issues, references); return { ok: issues.length === 0, issues, references: [...references] }; } function collectTestNodes(test: UnaryTest): Node[] { switch (test.kind) { case 'any': return []; case 'compare': return [test.operand]; case 'range': return [test.node]; case 'disjunction': return test.tests.flatMap(collectTestNodes); } } function walk(node: Node, issues: StaticIssue[], references: Set): void { switch (node.kind) { case 'literal': return; case 'path': references.add(node.segments.join('.')); return; case 'unary': walk(node.operand, issues, references); return; case 'binary': walk(node.left, issues, references); walk(node.right, issues, references); return; case 'list': for (const item of node.items) walk(item, issues, references); return; case 'range': walk(node.start, issues, references); walk(node.end, issues, references); return; case 'in': walk(node.value, issues, references); walk(node.candidates, issues, references); return; case 'call': if (!BUILTIN_FUNCTIONS.includes(node.name)) { issues.push({ kind: 'unknown-function', message: `未知函数 ${node.name}(S-FEEL 子集封顶,无外部调用)` }); } for (const arg of node.args) walk(arg, issues, references); return; } }