602 lines
25 KiB
Python
602 lines
25 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
r"""extract_checks.py — 断言静态提取器(验证集清单生成与出口对账)
|
||
|
||
机器提取宿主脚本的全部静态 check( 调用点(每个调用点记录:所在文件、所在函数、
|
||
行号、标签字符串字面量原文、cond 表达式原文),提供两种模式:
|
||
|
||
用法:
|
||
python tools/extract_checks.py --extract [--src <源文件>...] [--out <清单文件>] [--force]
|
||
生成验证集清单(已存在时拒写,除非 --force——冻结参照物防误覆)
|
||
python tools/extract_checks.py --diff <清单文件> [--src <源文件>...]
|
||
对当前源码再提取,与清单逐条比对——批 B 出口对账唯一工具
|
||
python tools/extract_checks.py --list-funcs [--src <源文件>...]
|
||
只打印函数清单与各函数条数(编账本辅助,不写文件)
|
||
|
||
断言源(--src 可多个;缺省=三宿主遍历):
|
||
game/tests/unit/**/*.gd + game/tests/integration/**/*.gd + game/tests/system_test.gd
|
||
批 B 出口对账用显式模式(--src 列全本批宿主文件)——缺省遍历含既有套件的 Kit.check
|
||
断言,对账时以「新增」出现(非迁移面),出口判定以显式模式为准。
|
||
|
||
提取口径:
|
||
- 静态 check( 调用点,两种通道都认:smoke/系统宿主的 check(cond, label)(cond 在前)
|
||
与单元/集成宿主的 Kit.check("label", cond)(点前缀=标签在前);`_check(` 等同名
|
||
前缀调用非标准通道,不取。
|
||
- 调用点可跨行:从 `check(` 起括号配平归集完整实参文本(() [] {} 三种括号都计深度);
|
||
字符串字面量内的括号/逗号/井号、行尾注释、转义序列均不参与结构判定(码位掩码单趟预扫)。
|
||
- cond 摘要=cond 原文跨行折叠为单行(连续空白并一空格);标签原文=标签实参里的字符串
|
||
字面量逐字保真(含引号与转义序列原样,禁改写);标签实参若带格式化/拼接后缀
|
||
(`"..." % [...]`),后缀折叠后以「↩」接排在标签列。
|
||
- md 表格单元格内 `|` 转义为 `\|`、`\` 转义为 `\\`(源码串内无这两序列的歧义形态,读回可还原)。
|
||
|
||
--diff 对账规则:只比对「标签+cond+计数」三要素(合同 §五出口机检);行号随迁移漂移、
|
||
序号重排、白话释义/处置列由维护回填,均不参与比对。对不上的条目分三类报告:
|
||
消失(清单有现码无)、新增(现码有清单无)、成对字面变更(cond 同标签异=标签变更;
|
||
标签同 cond 异=cond 变更——一条「消失」与一条「新增」同因一字之改时按成对呈现)。
|
||
退出码:0=全对齐;1=有差异;2=用法错或文件不可读。
|
||
"""
|
||
import argparse
|
||
import bisect
|
||
import re
|
||
import subprocess
|
||
import sys
|
||
from datetime import date
|
||
from pathlib import Path
|
||
|
||
ROOT = Path(__file__).resolve().parent.parent
|
||
# 断言源缺省=三宿主遍历:unit/**+integration/** 两目录递归 + 系统宿主单文件。
|
||
# 遍历面含既有套件的 Kit.check 断言(非本批迁移面,对账呈「新增」)——出口对账用 --src 显式列宿主。
|
||
DEFAULT_TREES = (
|
||
ROOT / "game" / "tests" / "unit",
|
||
ROOT / "game" / "tests" / "integration",
|
||
ROOT / "game" / "tests" / "system_test.gd",
|
||
)
|
||
DEFAULT_OUT = ROOT / "game" / "tests" / "验证集清单.md"
|
||
|
||
|
||
def default_sources():
|
||
"""三宿主遍历:两目录递归 *.gd + 系统宿主单文件,路径排序定序。"""
|
||
files = []
|
||
for p in DEFAULT_TREES:
|
||
if p.is_file():
|
||
files.append(p)
|
||
elif p.is_dir():
|
||
files.extend(sorted(p.rglob("*.gd")))
|
||
return sorted(files)
|
||
|
||
CALL_RE = re.compile(r"check\(")
|
||
DEF_RE = re.compile(r"^\s*(static\s+)?func\s+check\s*\(")
|
||
FUNC_RE = re.compile(r"^(static\s+)?func\s+([A-Za-z_][A-Za-z0-9_]*)")
|
||
LITERAL_RE = re.compile(r'"((?:[^"\\]|\\.)*)"')
|
||
OPENERS = "([{"
|
||
CLOSERS = ")]}"
|
||
CLOSER_OF = {")": "(", "]": "[", "}": "{"}
|
||
|
||
|
||
def fold(text: str) -> str:
|
||
"""跨行折叠:连续空白(含换行/制表/回车)并为一空格。对账归一口径。"""
|
||
return re.sub(r"\s+", " ", text).strip()
|
||
|
||
|
||
def esc_cell(text: str) -> str:
|
||
return text.replace("\\", "\\\\").replace("|", "\\|")
|
||
|
||
|
||
def unesc_cell(text: str) -> str:
|
||
"""esc_cell 的逆。源码串内无 `\\|`/`\\\\` 歧义形态(提取口径节),还原无歧义。"""
|
||
out = []
|
||
i = 0
|
||
while i < len(text):
|
||
if text[i] == "\\" and i + 1 < len(text):
|
||
out.append(text[i + 1])
|
||
i += 2
|
||
else:
|
||
out.append(text[i])
|
||
i += 1
|
||
return "".join(out)
|
||
|
||
|
||
def code_mask(joined: str) -> bytearray:
|
||
"""单趟预扫:1=码位(非字符串内部、非注释)。字符串与行尾注释位置置 0。"""
|
||
mask = bytearray(len(joined))
|
||
in_str = False
|
||
i = 0
|
||
n = len(joined)
|
||
while i < n:
|
||
ch = joined[i]
|
||
if in_str:
|
||
mask[i] = 0
|
||
if ch == "\\" and i + 1 < n:
|
||
mask[i + 1] = 0
|
||
i += 2
|
||
continue
|
||
if ch == '"':
|
||
in_str = False
|
||
i += 1
|
||
continue
|
||
if ch == '"':
|
||
in_str = True
|
||
mask[i] = 0
|
||
i += 1
|
||
continue
|
||
if ch == "#":
|
||
nl = joined.find("\n", i)
|
||
end = n if nl == -1 else nl
|
||
for j in range(i, end):
|
||
mask[j] = 0
|
||
i = end
|
||
continue
|
||
mask[i] = 1
|
||
i += 1
|
||
return mask
|
||
|
||
|
||
def func_table(lines):
|
||
"""顶层函数表:[(name, start_line_1based)]。lambda 归宿主函数。"""
|
||
funcs = []
|
||
for i, line in enumerate(lines, 1):
|
||
m = FUNC_RE.match(line)
|
||
if m:
|
||
funcs.append((m.group(2), i))
|
||
return funcs
|
||
|
||
|
||
def func_at(funcs, lineno):
|
||
name = "<module>"
|
||
for fn, start in funcs:
|
||
if start <= lineno:
|
||
name = fn
|
||
else:
|
||
break
|
||
return name
|
||
|
||
|
||
def split_args(body):
|
||
"""按括号配平拆实参(顶层逗号分隔)。body=check( 与最外层 ) 之间的文本(换行已并空格)。"""
|
||
args = []
|
||
depth = {"(": 0, "[": 0, "{": 0}
|
||
cur = []
|
||
in_str = False
|
||
i = 0
|
||
while i < len(body):
|
||
ch = body[i]
|
||
if in_str:
|
||
if ch == "\\" and i + 1 < len(body):
|
||
cur.append(body[i])
|
||
cur.append(body[i + 1]) # 转义序列原样入账
|
||
i += 2
|
||
continue
|
||
if ch == '"':
|
||
in_str = False
|
||
cur.append(ch)
|
||
elif ch == '"':
|
||
in_str = True
|
||
cur.append(ch)
|
||
elif ch in depth:
|
||
depth[ch] += 1
|
||
cur.append(ch)
|
||
elif ch in CLOSERS:
|
||
depth[CLOSER_OF[ch]] -= 1
|
||
cur.append(ch)
|
||
elif ch == "," and depth["("] == 0 and depth["["] == 0 and depth["{"] == 0:
|
||
args.append("".join(cur))
|
||
cur = []
|
||
else:
|
||
cur.append(ch)
|
||
i += 1
|
||
args.append("".join(cur))
|
||
return args
|
||
|
||
|
||
def collect_call(joined, start):
|
||
"""从 check( 的 start 起括号配平归集调用。返回 (body, end_pos);不配平返回 None。"""
|
||
i = start + len("check(")
|
||
depth = 1
|
||
in_str = False
|
||
parts = []
|
||
n = len(joined)
|
||
while i < n:
|
||
ch = joined[i]
|
||
if in_str:
|
||
if ch == "\\":
|
||
parts.append(joined[i:i + 2])
|
||
i += 2
|
||
continue
|
||
if ch == '"':
|
||
in_str = False
|
||
parts.append(ch)
|
||
elif ch == '"':
|
||
in_str = True
|
||
parts.append(ch)
|
||
elif ch in OPENERS:
|
||
depth += 1
|
||
parts.append(ch)
|
||
elif ch in CLOSERS:
|
||
depth -= 1
|
||
if depth == 0:
|
||
return "".join(parts), i
|
||
parts.append(ch)
|
||
elif ch == "#":
|
||
nl = joined.find("\n", i)
|
||
i = n if nl == -1 else nl # 行尾注释不进 body(并一空格)
|
||
parts.append(" ")
|
||
continue
|
||
else:
|
||
parts.append(ch if ch != "\n" else " ")
|
||
i += 1
|
||
return None, i
|
||
|
||
|
||
def extract(src_path: Path):
|
||
"""提取单文件全部静态 check( 调用点。返回(条目列表, 函数表),条目保持源码顺序。"""
|
||
src = src_path.read_text(encoding="utf-8")
|
||
lines = src.split("\n")
|
||
funcs = func_table(lines)
|
||
offsets = []
|
||
total = 0
|
||
for line in lines:
|
||
offsets.append(total)
|
||
total += len(line) + 1
|
||
joined = "\n".join(lines)
|
||
mask = code_mask(joined)
|
||
|
||
def line_of(pos):
|
||
i = bisect.bisect_right(offsets, pos) - 1
|
||
return i + 1
|
||
|
||
entries = []
|
||
for m in CALL_RE.finditer(joined):
|
||
if not mask[m.start()]:
|
||
continue # 字符串字面量内 / 注释里的 check( 字样
|
||
ln = line_of(m.start())
|
||
if DEF_RE.match(lines[ln - 1]):
|
||
continue # func check 定义行
|
||
prev = joined[m.start() - 1] if m.start() > 0 else ""
|
||
if prev and (prev.isalnum() or prev == "_"):
|
||
continue # `_check(` 等同名前缀——非标准通道
|
||
label_first = prev == "." # Kit.check("label", cond)——点前缀=标签在前
|
||
body, end_pos = collect_call(joined, m.start())
|
||
if body is None:
|
||
raise SystemExit("[extract_checks] FAIL:%s 行 %d 起的 check( 括号不配平到文件尾——先查解析缺口"
|
||
% (src_path.name, ln))
|
||
args = split_args(body)
|
||
if len(args) not in (2, 3):
|
||
raise SystemExit(
|
||
"[extract_checks] FAIL:%s 行 %d 的 check( 拆出 %d 个实参(预期 2/3)——解析缺口,先查再跑"
|
||
% (src_path.name, ln, len(args)))
|
||
if label_first:
|
||
label_expr, cond = args[0], args[1]
|
||
else:
|
||
cond, label_expr = args[0], args[1]
|
||
quiet = args[2] if len(args) == 3 else ""
|
||
lm = LITERAL_RE.search(label_expr)
|
||
if lm is None:
|
||
continue # 标签实参无字面量(wrapper 直传变量——既有套件先例 reward_overlay_test _req)
|
||
# 不取:无可指纹化面;冻结面对账不受影响(字面量损坏会以「消失」呈现)
|
||
entries.append({
|
||
"line": ln,
|
||
"end_line": line_of(min(end_pos, len(joined) - 1)),
|
||
"func": func_at(funcs, ln),
|
||
"label_literal": lm.group(0), # 含引号,原文逐字
|
||
"label_suffix": label_expr[lm.end():], # 格式化/拼接后缀(通常为空或 % ...)
|
||
"cond": cond,
|
||
"quiet": quiet,
|
||
})
|
||
return entries, funcs
|
||
|
||
|
||
def extract_all(src_paths):
|
||
"""多文件提取合并。返回(条目列表, {相对路径: 函数表});条目附加 src=相对路径。"""
|
||
entries = []
|
||
funcs_by_src = {}
|
||
for p in src_paths:
|
||
es, funcs = extract(p)
|
||
rel = p.relative_to(ROOT).as_posix() if p.is_relative_to(ROOT) else p.as_posix()
|
||
for e in es:
|
||
e["src"] = rel
|
||
entries.extend(es)
|
||
funcs_by_src[rel] = funcs
|
||
return entries, funcs_by_src
|
||
|
||
|
||
def label_cell(e):
|
||
"""标签列:字面量 ↩ 后缀(后缀空则只有字面量)。"""
|
||
sfx = fold(e["label_suffix"])
|
||
return e["label_literal"] + (" ↩ " + sfx if sfx else "")
|
||
|
||
|
||
def entry_key(e):
|
||
"""对账指纹=合同 §五三要素:cond 折叠 + 标签字面量(逐字)+ 后缀折叠。
|
||
quiet 不入指纹——它是 smoke 通道的打印档位(只影响 PASS 行打印,计数/红绿同口径),
|
||
Kit 通道无此参,迁移时按接线注记落通道转换;quiet 列保留展示,不参与对账。"""
|
||
return (fold(e["cond"]), e["label_literal"], fold(e["label_suffix"]))
|
||
|
||
|
||
def pad3(n):
|
||
return "S%02d" % n if n < 100 else "S%03d" % n
|
||
|
||
|
||
def base_commit():
|
||
try:
|
||
r = subprocess.run(["git", "rev-parse", "HEAD"], cwd=str(ROOT),
|
||
capture_output=True, text=True, encoding="utf-8")
|
||
if r.returncode == 0:
|
||
return r.stdout.strip()
|
||
except OSError:
|
||
pass
|
||
return "<git 不可用,未取得基点 commit>"
|
||
|
||
|
||
def render_list(entries, funcs_by_src, src_names, carry=None) -> str:
|
||
"""生成验证集清单。冻结列=机器提取;「白话释义/处置」两列:承接命中行用旧清单回填,
|
||
其余落(待填)由维护回填(回填不动冻结列)。多源按(源文件, 函数)分章——跨文件同名
|
||
函数不并章;章头行号=该源文件内行号。头注全部由当次数据生成,禁批次专名与写死计数。"""
|
||
carry = carry or {}
|
||
out = []
|
||
out.append("# 验证集清单(断言快照·冻结)")
|
||
out.append("")
|
||
out.append("> 生成日期:%s · 基点 commit:`%s` · 源:%s" % (date.today().isoformat(), base_commit(), "、".join(src_names)))
|
||
out.append("> 提取口径:静态 `check(` 调用点,括号配平归集(可跨行);缺省源=三宿主遍历")
|
||
out.append("> (unit/**+integration/**+system_test.gd)。本基点实测 %d 个静态调用点;行级" % len(entries))
|
||
out.append("> `grep -c \"check(\"` 是另一口径(含 `func check(` 定义行与注释行),两口径各自浮动,")
|
||
out.append("> 禁互相换算(tests/README「断言计数双口径」)。运行时计数大于静态计数=循环展开条目。")
|
||
out.append("> 生成命令:`python tools/extract_checks.py --extract`(可复现;重新生成须 --force;")
|
||
out.append("> 承接旧清单回填列须 --carry-from <旧清单>)。")
|
||
out.append("> 冻结纪律:每条的「标签原文」「cond 摘要」「quiet」三列为机器提取逐字保真,禁任何改写;")
|
||
out.append("> cond 摘要=原文跨行折叠单行(连续空白并一空格),单元格内 `|`→`\\|`、`\\`→`\\\\` 转义,读回还原。")
|
||
out.append("> 「白话释义」「处置」两列由维护回填(未承接行为(待填)),不属冻结面。")
|
||
out.append("> 对账:`python tools/extract_checks.py --diff game/tests/验证集清单.md`(缺省三宿主源)")
|
||
out.append("> ——标签/cond/计数三对齐才算同步完。维护义务:断言新增/删除/改判必须同步本清单")
|
||
out.append("> (test-conventions §8.3 维护锚)。")
|
||
out.append("")
|
||
seq = 0
|
||
by_gf = []
|
||
for e in entries:
|
||
key = (e["src"], e["func"])
|
||
if by_gf and by_gf[-1][0] == key:
|
||
by_gf[-1][1].append(e)
|
||
else:
|
||
by_gf.append((key, [e]))
|
||
for idx, ((src, fn), es) in enumerate(by_gf, 1):
|
||
start, end = es[0]["line"], es[-1]["end_line"]
|
||
out.append("## %s %s(行 %d–%d · %d 条 · 源 %s · 去向:待分类回填)" % (pad3(idx), fn, start, end, len(es), src))
|
||
out.append("")
|
||
out.append("| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet |")
|
||
out.append("|---|---|---|---|---|---|---|")
|
||
for e in es:
|
||
seq += 1
|
||
hit = carry.get((fold(e["cond"]), e["label_literal"], fold(e["label_suffix"])))
|
||
disp = esc_cell(hit[0]) if hit else "(待填)"
|
||
act = esc_cell(hit[1]) if hit else ""
|
||
out.append("| %d | %d | %s | %s | %s | %s | %s |" % (
|
||
seq, e["line"], esc_cell(label_cell(e)), esc_cell(fold(e["cond"])), disp, act,
|
||
esc_cell(fold(e["quiet"]))))
|
||
out.append("")
|
||
out.append("---")
|
||
out.append("全源共 %d 章、%d 条(静态调用点口径)。" % (len(by_gf), seq))
|
||
return "\n".join(out) + "\n"
|
||
|
||
|
||
CELL_SPLIT_RE = re.compile(r"(?<!\\)\|")
|
||
|
||
|
||
def parse_list(path: Path):
|
||
"""读回清单:返回条目列表(键与 entry_key 同构;section 供报告归组)。"""
|
||
entries = []
|
||
cur_section = None
|
||
for i, line in enumerate(path.read_text(encoding="utf-8").split("\n"), 1):
|
||
if line.startswith("## "):
|
||
m = re.match(r"## \S+ (\S+)(行", line)
|
||
cur_section = m.group(1) if m else line[3:].strip()
|
||
continue
|
||
if not line.startswith("|"):
|
||
continue
|
||
cells = [c.strip() for c in CELL_SPLIT_RE.split(line)[1:-1]]
|
||
if len(cells) != 7 or not cells[0].isdigit() or cells[0] == "序号":
|
||
continue # 表头/分隔行
|
||
label_raw = unesc_cell(cells[2])
|
||
cond = unesc_cell(cells[3])
|
||
quiet = unesc_cell(cells[6])
|
||
disp = unesc_cell(cells[4])
|
||
action = unesc_cell(cells[5])
|
||
lit_m = LITERAL_RE.match(label_raw)
|
||
literal = lit_m.group(0) if lit_m else label_raw
|
||
suffix = label_raw[lit_m.end():].strip() if lit_m else ""
|
||
suffix = suffix[1:].strip() if suffix.startswith("↩") else suffix
|
||
entries.append({
|
||
"md_line": i,
|
||
"section": cur_section,
|
||
"label_literal": literal,
|
||
"label_suffix": suffix,
|
||
"cond": cond,
|
||
"quiet": quiet,
|
||
"disp": disp,
|
||
"action": action,
|
||
})
|
||
return entries
|
||
|
||
|
||
CARRY_RENAME = ("node_map_templates_test.gd", "node_map_determinism_test.gd")
|
||
|
||
|
||
def carry_cells(frozen_rows):
|
||
"""旧清单行 → 承接映射:指纹(与 entry_key 同构)→(白话释义, 处置)。
|
||
承接文本内的旧宿主文件名一并映射为新名(唯一在册更名对象)。"""
|
||
m = {}
|
||
for r in frozen_rows:
|
||
key = (fold(r["cond"]), r["label_literal"], fold(r["label_suffix"]))
|
||
m[key] = (r.get("disp", "").replace(*CARRY_RENAME), r.get("action", "").replace(*CARRY_RENAME))
|
||
return m
|
||
|
||
|
||
def _short(s, n=80):
|
||
s = s.replace("\n", " ")
|
||
return s if len(s) <= n else s[:n] + "…"
|
||
|
||
|
||
def diff_report(list_path: Path, src_paths) -> int:
|
||
cur, _funcs = extract_all(src_paths)
|
||
frozen = parse_list(list_path)
|
||
cur_keys = {}
|
||
for e in cur:
|
||
cur_keys.setdefault(entry_key(e), []).append(e)
|
||
frozen_keys = {}
|
||
for e in frozen:
|
||
frozen_keys.setdefault(entry_key(e), []).append(e)
|
||
missing = [k for k in frozen_keys if k not in cur_keys]
|
||
added = [k for k in cur_keys if k not in frozen_keys]
|
||
# 成对归类:cond 同→标签变更;标签同→cond 变更
|
||
label_changes, cond_changes = [], []
|
||
used_added = set()
|
||
rest_missing = []
|
||
for k in missing:
|
||
cond, lit, sfx = k
|
||
pair = next((ka for ka in added if ka not in used_added and ka[0] == cond), None)
|
||
if pair is not None:
|
||
used_added.add(pair)
|
||
label_changes.append((k, pair))
|
||
continue
|
||
pair = next((ka for ka in added if ka not in used_added and ka[1] == lit and ka[2] == sfx), None)
|
||
if pair is not None:
|
||
used_added.add(pair)
|
||
cond_changes.append((k, pair))
|
||
continue
|
||
rest_missing.append(k)
|
||
rest_added = [k for k in added if k not in used_added]
|
||
ok = True
|
||
print("[extract_checks] 对账:%s vs 现提取 %d 个源文件" % (list_path.name, len(src_paths)))
|
||
print(" 清单条数=%d 现提取条数=%d %s" % (
|
||
len(frozen), len(cur), "(计数对齐)" if len(frozen) == len(cur) else "(计数不齐)"))
|
||
|
||
def show(k, tag, q=""):
|
||
cond, lit, sfx = k
|
||
s = " %s cond=%s label=%s" % (tag, _short(cond), _short(lit))
|
||
if sfx:
|
||
s += " suffix=%s" % _short(fold(sfx))
|
||
if q:
|
||
s += " quiet=%s" % fold(q)
|
||
print(s)
|
||
|
||
if label_changes:
|
||
ok = False
|
||
print(" 标签字面变更 %d 对(cond 同、标签异):" % len(label_changes))
|
||
for old, new in label_changes:
|
||
show(old, "-", frozen_keys[old][0].get("quiet", ""))
|
||
show(new, "+", cur_keys[new][0].get("quiet", ""))
|
||
if cond_changes:
|
||
ok = False
|
||
print(" cond 字面变更 %d 对(标签同、cond 异):" % len(cond_changes))
|
||
for old, new in cond_changes:
|
||
show(old, "-", frozen_keys[old][0].get("quiet", ""))
|
||
show(new, "+", cur_keys[new][0].get("quiet", ""))
|
||
if rest_missing:
|
||
ok = False
|
||
print(" 消失 %d 条(清单有、现码无):" % len(rest_missing))
|
||
for k in rest_missing:
|
||
print(" - [%s]" % frozen_keys[k][0].get("section", "?"), end="")
|
||
show(k, "", frozen_keys[k][0].get("quiet", ""))
|
||
if rest_added:
|
||
ok = False
|
||
print(" 新增 %d 条(现码有、清单无):" % len(rest_added))
|
||
for k in rest_added:
|
||
es = cur_keys[k][0]
|
||
print(" + [src %s func %s @%d]" % (es["src"], es["func"], es["line"]), end="")
|
||
show(k, "", es.get("quiet", ""))
|
||
if ok:
|
||
print("[extract_checks] PASS——标签/cond/计数三对齐,零差异")
|
||
return 0
|
||
print("[extract_checks] FAIL——见上差异清单")
|
||
return 1
|
||
|
||
|
||
def list_funcs(entries, funcs_by_src):
|
||
for src, funcs in funcs_by_src.items():
|
||
by_func = []
|
||
for e in entries:
|
||
if e["src"] != src:
|
||
continue
|
||
if by_func and by_func[-1][0] == e["func"]:
|
||
by_func[-1][1] = by_func[-1][1] + 1
|
||
else:
|
||
by_func.append([e["func"], 1])
|
||
print("=== %s ===" % src)
|
||
checked = {f for f, _ in by_func}
|
||
for fn, n in by_func:
|
||
print("%s\t%d" % (fn, n))
|
||
print("--- 无断言函数(helper,随套迁移)---")
|
||
for fn, ln in funcs:
|
||
if fn not in checked:
|
||
print("%s\t@%d" % (fn, ln))
|
||
|
||
|
||
def resolve_sources(src_args):
|
||
"""--src 列表 → Path 列表(相对路径按仓库根解析);缺省=三宿主遍历。任一文件不可读即 None。"""
|
||
if src_args:
|
||
paths = [Path(s) if Path(s).is_absolute() else ROOT / s for s in src_args]
|
||
else:
|
||
paths = default_sources()
|
||
if not paths:
|
||
print("[extract_checks] FAIL:断言源为空(三宿主遍历无命中?)")
|
||
return None
|
||
for p in paths:
|
||
if not p.is_file():
|
||
print("[extract_checks] FAIL:源文件不可读:%s" % p)
|
||
return None
|
||
return paths
|
||
|
||
|
||
def main() -> int:
|
||
if hasattr(sys.stdout, "reconfigure"):
|
||
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||
ap = argparse.ArgumentParser(description="断言静态提取器(清单生成/对账)")
|
||
ap.add_argument("--extract", action="store_true", help="生成验证集清单")
|
||
ap.add_argument("--diff", metavar="清单文件", help="与清单逐条对账(批 B 出口机检)")
|
||
ap.add_argument("--list-funcs", action="store_true", help="打印函数清单与条数(编账本辅助)")
|
||
ap.add_argument("--src", nargs="+", default=None,
|
||
help="断言源文件(可多个;缺省=三宿主遍历 unit/**+integration/**+system_test.gd)")
|
||
ap.add_argument("--out", default=str(DEFAULT_OUT), help="--extract 输出路径")
|
||
ap.add_argument("--force", action="store_true", help="输出文件已存在时允许覆盖")
|
||
ap.add_argument("--carry-from", default=None, metavar="旧清单",
|
||
help="--extract 时按指纹承接旧清单「白话释义/处置」两列(旧宿主文件名映射为新名)")
|
||
args = ap.parse_args()
|
||
if sum(bool(x) for x in (args.extract, args.diff, args.list_funcs)) != 1:
|
||
print("[extract_checks] 用法:--extract / --diff <清单文件> / --list-funcs 三选一")
|
||
return 2
|
||
src_paths = resolve_sources(args.src)
|
||
if src_paths is None:
|
||
return 2
|
||
if args.extract:
|
||
out = Path(args.out)
|
||
if out.exists() and not args.force:
|
||
print("[extract_checks] FAIL:清单已存在(%s)——冻结参照物防误覆,重生成须加 --force" % out)
|
||
return 2
|
||
carry = {}
|
||
if args.carry_from:
|
||
cf = Path(args.carry_from)
|
||
if not cf.is_file():
|
||
print("[extract_checks] FAIL:--carry-from 旧清单不可读:%s" % cf)
|
||
return 2
|
||
old_rows = parse_list(cf)
|
||
carry = carry_cells(old_rows)
|
||
print("[extract_checks] 旧清单 %d 行 → 承接指纹 %d 个" % (len(old_rows), len(carry)))
|
||
entries, funcs_by_src = extract_all(src_paths)
|
||
out.write_text(render_list(entries, funcs_by_src, [p.name for p in src_paths], carry), encoding="utf-8", newline="\n")
|
||
hitn = sum(1 for e in entries
|
||
if (fold(e["cond"]), e["label_literal"], fold(e["label_suffix"])) in carry)
|
||
print("[extract_checks] 已生成 %s(%d 个源文件、%d 个调用点、承接回填 %d 行)"
|
||
% (out, len(src_paths), len(entries), hitn))
|
||
return 0
|
||
if args.diff:
|
||
lp = Path(args.diff)
|
||
if not lp.is_file():
|
||
print("[extract_checks] FAIL:清单不可读:%s" % lp)
|
||
return 2
|
||
return diff_report(lp, src_paths)
|
||
entries, funcs_by_src = extract_all(src_paths)
|
||
list_funcs(entries, funcs_by_src)
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|