75 lines
3.1 KiB
Python
75 lines
3.1 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
fix_i18n_w1_dspy.py — dspy 安全修复 W1(保守策略)
|
||
只处理纯字面量 "text": "中文"(match 后第一个非空白字符是 , 或 }),
|
||
跳过所有拼接(+)、多行隐式连接(后跟换行+字符串)、变量插值——那些记入 manual 清单。
|
||
|
||
替换: "text": "X" → "otext": "X", "text": "X", "i18n": True
|
||
引号风格跟随原文(' 或 ")。
|
||
用法: python3 scripts/fix_i18n_w1_dspy.py /tmp/i18n.json [--dry-run]
|
||
"""
|
||
import json, re, sys
|
||
from collections import defaultdict
|
||
|
||
TEXT_RE = re.compile(r'''(['"])text\1\s*:\s*(['"])((?:[^'"\\]|\\.)*)\2''')
|
||
|
||
def safe_literal(raw, m):
|
||
"""match 后第一个非空白字符必须是 , 或 }(排除拼接与多行隐式连接)"""
|
||
j = m.end()
|
||
while j < len(raw) and raw[j] in ' \t':
|
||
j += 1
|
||
return j < len(raw) and raw[j] in ',}'
|
||
|
||
def main():
|
||
report = json.load(open(sys.argv[1], encoding='utf-8'))
|
||
dry = '--dry-run' in sys.argv
|
||
viols = [v for v in report['violations']
|
||
if v['code'] == 'W1' and not v['git_ignored'] and v['file'].endswith('.dspy')]
|
||
by_file = defaultdict(list)
|
||
for v in viols:
|
||
by_file[v['file']].append(v)
|
||
|
||
fixed = manual = 0
|
||
manual_list = []
|
||
for fp, vs in sorted(by_file.items()):
|
||
raw = open(fp, encoding='utf-8').read()
|
||
# 从后往前
|
||
changed = False
|
||
for v in sorted(vs, key=lambda x: -x['pos']):
|
||
pos = v['pos']
|
||
m = TEXT_RE.match(raw, pos)
|
||
if not m:
|
||
# otext 无 i18n 的情形
|
||
mo = re.compile(r'''(['"])otext\1\s*:\s*(['"])((?:[^'"\\]|\\.)*)\2''').match(raw, pos)
|
||
if mo and not re.search(r"['\"]i18n['\"]\s*:\s*True", raw[mo.end():mo.end()+60]):
|
||
if safe_literal(raw, mo):
|
||
ins = f', {mo.group(1)}i18n{mo.group(1)}: True'
|
||
if not dry:
|
||
raw = raw[:mo.end()] + ins + raw[mo.end():]
|
||
fixed += 1; changed = True
|
||
else:
|
||
manual += 1; manual_list.append((fp, pos, 'otext动态', v['text'][:50]))
|
||
else:
|
||
manual += 1; manual_list.append((fp, pos, '无法定位', v['text'][:50]))
|
||
continue
|
||
val = m.group(3)
|
||
if not safe_literal(raw, m):
|
||
manual += 1
|
||
manual_list.append((fp, pos, '拼接/多行串', val[:50]))
|
||
continue
|
||
q1, q2 = m.group(1), m.group(2)
|
||
seg = raw[m.start():m.end()]
|
||
new_seg = f'{q1}otext{q1}: {q2}{val}{q2}, {q1}text{q1}: {q2}{val}{q2}, {q1}i18n{q1}: True'
|
||
if not dry:
|
||
raw = raw[:m.start()] + new_seg + raw[m.end():]
|
||
fixed += 1; changed = True
|
||
if changed and not dry:
|
||
open(fp, 'w', encoding='utf-8').write(raw)
|
||
|
||
print(f'{"DRY-RUN " if dry else ""}fixed={fixed} manual={manual} files={len(by_file)}')
|
||
for mm in manual_list[:40]:
|
||
print(' MANUAL', mm[0].split('/repos/')[-1], f'@{mm[1]}', mm[2], '|', mm[3])
|
||
|
||
if __name__ == '__main__':
|
||
main()
|