import { Expression, isExpression, type Scope, type ExpressionSource, type ExpressionOrValue, type VmPrimitive, type VmValue, } from '@cloudpss/expression'; import { migrateMathJs } from '@cloudpss/expression/migrate'; import { toArgumentValue, type Choice, type ConditionFunction, type Parameter, type ParameterDecoration, type ParameterDecorationGroup, type ParameterGroup, type Variable, } from '@cloudpss/expression/definitions'; import type { ModelConfiguration, PinDefinition } from '../model/index.js'; import type { FuncConfiguration } from '../function/index.js'; /** 升级算例 */ export class V5Migrator { /** 迁移模板 */ migrateTemplate(template: string, context: Scope): string { if (!template.includes('$')) return template; return template.replaceAll( /\$(([_a-z]\w*)|{([^}]+?)}|`([^`]+?)`)/gi, (match: string, _: string, argName?: string, argExp?: string, argExp2?: string): string => { if (argName) return match; const exp = (argExp ?? argExp2) as ExpressionSource; let m: string = migrateMathJs(exp, false, context); if (/\breturn\b/.test(m)) { m = `(fn {\n${m}\n})()`; } if (m.includes('\n')) m = `\n${m}\n`; return `\${${m}}`; }, ); } /** 迁移 condition */ migrateCondition(v: { condition?: string | boolean | ConditionFunction }, context: Scope): void { this.migrateExpressionSource('condition', v, context, true); } /** 迁移 dim */ migrateDim(v: { dim?: Array> }, context: Scope): void { if (v.dim == null) return; for (let i = 0; i < v.dim.length; i++) { const d = v.dim[i]; v.dim[i] = migrateMathJs(String(d ?? '') as ExpressionSource, false, context); } } /** 迁移一般表达式 */ migrateExpression< K extends string | number, T extends VmValue, C extends Partial>>>, >( key: K, v: C extends Partial>>> ? never : C, context: Scope, condition = false, ): void { const expr = v[key]; if (expr == null) return; if (!isExpression(expr)) return; v[key] = Expression(migrateMathJs(expr.source, condition, context)) as never; } /** 迁移一般表达式 */ migrateExpressionSource( key: K, // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type v: Partial>, context: Scope, condition = false, ): void { const expr = v[key]; if (expr == null) return; if (typeof expr != 'string') return; v[key] = migrateMathJs(expr as ExpressionSource, condition, context); } /** 迁移 config */ migrateConfig(config: ModelConfiguration | FuncConfiguration, context: Scope): void { for (const key in config.args) { this.migrateExpression(key, config.args, context); } if ('pins' in config) { for (const key in config.pins) { this.migrateExpression(key, config.pins, context); } } } /** 迁移 variables */ migrateVariables(variables: Variable[] | null | undefined, context: Scope): void { if (variables == null) return; if (!Array.isArray(variables)) throw new Error('Invalid variables'); const sorting = new Set<{ key: string; reg: RegExp; source: string; var: Variable }>(); const sorted = []; // 1. 添加非表达式项 for (const v of variables) { if (!isExpression(v.value)) { sorted.push(v); } else { sorting.add({ key: v.key, // assume variable key is valid identifier, no need to escape reg: new RegExp(String.raw`[$.]\b${v.key}\b`, 'u'), source: v.value.source, var: v, }); } } // 2. 添加表达式项 while (sorting.size > 0) { let found = false; for (const v of sorting) { let hasUnevaluatedDep = false; for (const dep of sorting) { if (dep.reg.test(v.source)) { hasUnevaluatedDep = true; break; } } if (!hasUnevaluatedDep) { sorted.push(v.var); sorting.delete(v); found = true; continue; } } if (!found) { // 如果没有找到可迁移的表达式,直接添加剩余的 for (const v of sorting) { sorted.push(v.var); } sorting.clear(); } } for (const v of sorted) { if (v == null) continue; this.migrateExpression('value', v, context); } } /** 迁移 parameter decorations */ migrateParameterDecorations( decorations: ParameterDecorationGroup | ParameterDecoration | undefined, context: Scope, ): void { if (decorations == null) return; this.migrateCondition(decorations, context); if ('items' in decorations) { for (const item of decorations.items) { this.migrateParameterDecorations(item, context); } } } /** 迁移 parameter choices */ private migrateParameterChoices(p: Parameter, signature: string | undefined, context: Scope): void { if (p.type !== 'logical' && p.type !== 'multiSelect' && p.type !== 'choice') return; if (typeof p.choices == 'function') return; if (isExpression(p.choices)) { p.choices = Expression(migrateMathJs(p.choices.source, false, context)); return; } if (!signature) { const keys = (p.choices ?? []).map((c) => c?.key).filter((k) => k != null); if (!keys.length && p.value != null) { if (typeof p.value == 'string') { signature = 's'; } else if (typeof p.value == 'number') { signature = 'f'; } else if (typeof p.value == 'boolean') { signature = 'b'; } } if (!signature && keys.length) { if (keys.every((k) => typeof k == 'number')) { signature = 'f'; } else if (keys.every((k) => typeof k == 'boolean')) { signature = 'b'; } else { signature = 's'; } } } // 填充 logical 类型的 choices if (p.type === 'logical' && !(!signature || signature === 'b')) { const ret = (p.choices ?? []) as Choice[]; if (ret.length > 2) { ret.length = 2; } else { while (ret.length < 2) { ret.push({ key: ret.length, name: '', description: '', condition: undefined }); } } p.choices = ret as [false: Choice, true: Choice]; } if (!Array.isArray(p.choices) || !signature) return; let converter = undefined; switch (signature) { case 's': case 'L': { converter = String; break; } case 'f': case 'L': { converter = Number; break; } case 'b': case 'L': { converter = (v: unknown) => { if (!v) return false; if (typeof v != 'string') return Boolean(v); const n = Number(v); if (!Number.isNaN(n)) return n !== 0; return v.toLowerCase() !== 'false'; }; break; } } for (let i = 0; i < p.choices.length; i++) { const choice = p.choices[i]; if (choice == null) continue; this.migrateCondition(choice, context); if (converter) { const oldKey = choice.key ?? i; choice.key = converter(oldKey); } } if (p.value == null && p.choices.length > 0) { p.value = p.choices[0].key; } else if (converter && p.value != null) { p.value = converter(p.value); } } /** 迁移 parameter */ migrateParameter(p: Parameter, context: Scope): void { const { signature } = p as Parameter & { signature?: string }; delete (p as Parameter & { signature?: string }).signature; this.migrateCondition(p, context); this.migrateParameterDecorations(p.before, context); this.migrateParameterDecorations(p.after, context); if (p.constraints) { for (const c of p.constraints) { this.migrateCondition(c, context); c.level ??= 'error'; } } this.migrateParameterChoices(p, signature, context); if ('columns' in p && Array.isArray(p.columns)) { for (const col of p.columns) { // 列定义的 key 在 v5 中表示字段名,之前仅为文档 col.key = ''; this.migrateParameter(col, context); } } if ('dim' in p && Array.isArray(p.dim)) { this.migrateDim(p, context); } if (p.type === 'record') { for (const f of p.items) { this.migrateParameter(f, context); } } else if (p.type === 'grouped') { this.migrateParameters(p.items, context); } if (isExpression(p.value)) { p.value = Expression(migrateMathJs(p.value.source, false, context)); } else if (p.value != null) { // 尝试修复之前定义里的类型错误 const migVal = toArgumentValue(p.value, p); if (migVal != null) p.value = migVal as never; } } /** 迁移 parameters */ migrateParameters(groups: ParameterGroup[], context: Scope): void { for (const pg of groups) { this.migrateCondition(pg, context); this.migrateParameterDecorations(pg.before, context); this.migrateParameterDecorations(pg.after, context); for (const p of pg.items) { this.migrateParameter(p, context); } } } /** 迁移 pins */ migratePinDefinitions(pins: PinDefinition[], context: Scope): void { for (const pin of pins) { this.migrateCondition(pin, context); this.migrateDim(pin, context); } } }