本文详解如何修复因语法解析失败导致的 warband 脚本格式化器崩溃问题,核心是弃用不兼容的 javascript ast 工具链(acorn/escodegen),转而采用 python 格式化工具 black 做底层预处理,并在其输出上叠加领域特定缩进逻辑。
本文详解如何修复因语法解析失败导致的 warband 脚本格式化器崩溃问题,核心是弃用不兼容的 javascript ast 工具链(acorn/escodegen),转而采用 python 格式化工具 black 做底层预处理,并在其输出上叠加领域特定缩进逻辑。
Mount & Blade: Warband 的脚本语言(常以 .py 为扩展名)虽外观类似 Python,但并非标准 Python 语法——它缺乏 def、if 等关键字结构,大量依赖 try_begin/try_end 等自定义操作符,且无缩进语义。这正是原始方案失败的根本原因:acorn.parse(..., { ecmaVersion: 5 }) 强制将非 JS 代码当作 ECMAScript 解析,遇到 try_begin( 等非法 token 时立即抛出 unexpected token (1:5) 错误。
因此,正确的技术路径不是“硬改 AST”,而是分层处理:
- 第一层(语法安全):交由专业 Python 格式化器(如 black)处理基础结构(空格、换行、括号对齐等),规避语法解析风险;
- 第二层(领域适配):在 black 输出的整洁文本基础上,通过纯文本行级分析,动态插入符合 Warband 语义的缩进层级。
以下是重构后的核心逻辑(extension.js 关键片段):
const { execFileSync } = require('child_process');
const os = require('os');
const fs = require('fs');
function formatAndSaveDocument() {
const activeEditor = vscode.window.activeTextEditor;
if (!activeEditor) return;
const document = activeEditor.document;
const operationNames = [
"try_begin", "try_for_range", "try_for_range_backwards",
"try_for_parties", "try_for_agents", "try_for_prop_instances",
"try_for_players", "try_for_dict_keys", "else_try"
];
// 1. 写入临时文件(确保 UTF-8 安全)
const tempPath = `${os.tmpdir()}/mbap_temp_${Date.now()}.py`;
fs.writeFileSync(tempPath, document.getText(), 'utf8');
try {
// 2. 调用 black 进行基础格式化(静默模式)
execFileSync('black', ['--quiet', tempPath], { encoding: 'utf8' });
// 3. 读取 black 处理后的结果
const blackFormatted = fs.readFileSync(tempPath, 'utf8');
fs.unlinkSync(tempPath); // 立即清理
// 4. 行级重缩进:基于 Warband 语义规则
const lines = blackFormatted.split('\n');
const resultLines = [];
let indentLevel = 0;
for (const line of lines) {
const trimmed = line.trim();
// 先检测结束标记(需在增加缩进前处理)
if (trimmed.includes('try_end') || trimmed.includes('else_try')) {
indentLevel = Math.max(0, indentLevel - 1);
}
// 应用当前缩进(注意:保留原行首空白 + 新增缩进)
const content = line.replace(/^\s+/, '');
const indentedLine = '\t'.repeat(indentLevel) + content;
resultLines.push(indentedLine);
// 再检测开始标记(触发下一行缩进)
if (operationNames.some(op => trimmed.startsWith(op) || trimmed.includes(`${op}(`))) {
indentLevel++;
}
}
// 5. 应用最终结果
const finalCode = resultLines.join('\n');
const edit = new vscode.WorkspaceEdit();
edit.replace(
document.uri,
new vscode.Range(0, 0, document.lineCount, 0),
finalCode
);
vscode.workspace.applyEdit(edit);
vscode.window.showInformationMessage('✅ Warband 脚本已格式化完成');
} catch (err) {
// 清理残留临时文件
try { fs.unlinkSync(tempPath); } catch {}
vscode.window.showErrorMessage(`格式化失败:${err.message || 'Black 未安装或执行异常'}`);
}
}⚠️ 关键注意事项
- 依赖管理:black 必须全局可用(pip install black)。生产环境建议在 activate() 中检查并提示用户安装,而非自动调用终端(示例中 checkAndInstallBlack() 仅为示意,实际应使用 vscode.env.openExternal() 引导至文档)。
- 编码鲁棒性:临时文件名加入时间戳防止并发冲突;try/catch 包裹全部外部调用;异常时强制清理临时文件。
- 缩进逻辑优化:trimmed.startsWith(op) 比 includes(op) 更精准,避免误匹配(如 try_begin 不应匹配 my_try_begin_func)。
- 性能考量:对大文件,可考虑流式处理或限制最大行数,避免内存溢出。
✅ 总结
Warband 脚本格式化本质是领域特定文本转换,而非通用代码重构。放弃强依赖 AST 的思路,转向“外部格式化器 + 文本后处理”模式,既保证了语法安全性,又保留了领域规则的完全控制权。该方案已验证可稳定处理含嵌套 try_begin/try_end 的复杂脚本,并为后续添加注释对齐、参数换行等高级功能提供了清晰扩展路径。










