From 6349740c0a60b03a20efda98ad2ab3f520e2135b Mon Sep 17 00:00:00 2001 From: doublex001 <16157008+doublex001@user.noreply.gitee.com> Date: Wed, 30 Sep 2026 14:48:15 +0800 Subject: [PATCH] =?UTF-8?q?fix(scripts):=20=E4=BF=AE=E6=AD=A3=E8=A6=86?= =?UTF-8?q?=E7=9B=96=E7=8E=87=E5=AE=A1=E8=AE=A1=E7=9A=84=E6=B3=9B=E5=9E=8B?= =?UTF-8?q?=E8=A7=A3=E6=9E=90=E7=BC=BA=E9=99=B7=EF=BC=8C=E8=A6=86=E7=9B=96?= =?UTF-8?q?=E7=8E=87=E4=B8=8D=E5=86=8D=E8=A2=AB=E4=BD=8E=E4=BC=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 原用正则 `request\s*(?:<[^>]*>)?\s*\(` 解析调用,泛型遇到嵌套 `>` (如 request<{items: Omit[]}>)在第一个 `>` 就收尾,随后要求 '(' 而实际是 ' & {',整个调用被静默漏检 - 改为平衡尖括号解析(_skip_generics,忽略 => 箭头),并显式标注 「路径非字面量则无法静态判定」的跳过条件 - 影响:已接入 83 → 85,未接入 165 → 163(修正前后差 2 个端点) --- scripts/audit_coverage.py | 69 ++++++++++++++++++++++++++++++++------- 1 file changed, 58 insertions(+), 11 deletions(-) diff --git a/scripts/audit_coverage.py b/scripts/audit_coverage.py index 6aa088b..73aa585 100644 --- a/scripts/audit_coverage.py +++ b/scripts/audit_coverage.py @@ -46,17 +46,58 @@ def load_spec(): return ops +def _skip_generics(text: str, i: int) -> int: + """从 text[i] 开始(i 指向 '<')跳过一层平衡尖括号,返回 '>' 之后的位置。 + + ⚠️ 不能简单用正则 `<[^>]*>`:泛型里常嵌套 `>`,例如 + request<{ items: Omit[] }>('/applications/me', ...) + ↑ 正则会在第一个 '>' 就收尾,随后要求 '(' 而实际是 ' & {', + 整个调用被静默漏检 → 覆盖率被系统性低估。 + 同时要忽略 `=>`(箭头函数类型),它的 '>' 不参与配对。 + 括号不平衡时返回 -1,表示这不是一个泛型。 + """ + depth = 0 + k = i + n = len(text) + while k < n: + ch = text[k] + if ch == ">" and k > 0 and text[k - 1] == "=": # `=>` 箭头,跳过 + k += 1 + continue + if ch == "<": + depth += 1 + elif ch == ">": + depth -= 1 + if depth == 0: + return k + 1 + k += 1 + return -1 + + +def _call_paren(text: str, pos: int) -> int: + """text[pos:] 以 `request` 开头时,返回其后调用括号 '(' 的下标,否则 -1。""" + i = pos + len("request") + n = len(text) + while i < n and text[i].isspace(): + i += 1 + if i < n and text[i] == "<": + i = _skip_generics(text, i) + if i < 0: + return -1 + while i < n and text[i].isspace(): + i += 1 + return i if i < n and text[i] == "(" else -1 + + def load_frontend(): """从 api.ts / auth.ts 抽取 request() 调用:返回 [(method, normpath, funcname, file, line)] - 注意:泛型参数常跨多行书写(如 request<{ accessToken: string;\n ... }>('/auth/login')), - 因此必须在「整文件文本」上匹配,逐行扫描会整片漏掉。 + 注意:泛型参数常跨多行书写,且可能嵌套 `>`,因此必须在「整文件文本」上做 + 平衡括号解析,逐行扫描或朴素正则都会漏检。 """ calls = [] - call_re = re.compile( - r"request\s*(?:<[^>]*>)?\s*\(\s*(?P['\"`])(?P[^'\"`]+)(?P=q)", - re.DOTALL, - ) + head_re = re.compile(r"\brequest\b") + arg_re = re.compile(r"\s*(?P['\"`])(?P[^'\"`]+)(?P=q)") func_re = re.compile( r"^\s*(?:export\s+)?(?:async\s+)?function\s+(?P\w+)\s*\(", re.MULTILINE ) @@ -65,18 +106,24 @@ def load_frontend(): text = f.read() # 函数名 → 起始偏移,用于把调用归属到所在函数 func_starts = [(m.start(), m.group("name")) for m in func_re.finditer(text)] - for cm in call_re.finditer(text): - path = norm(cm.group("path")) - after = text[cm.end() : cm.end() + 200] + for hm in head_re.finditer(text): + paren = _call_paren(text, hm.start()) + if paren < 0: + continue + am = arg_re.match(text, paren + 1) + if not am: + continue # 路径不是字面量(变量/拼接),无法静态判定 + path = norm(am.group("path")) + after = text[am.end() : am.end() + 200] mm = re.search(r"method:\s*['\"](GET|POST|PUT|PATCH|DELETE)['\"]", after) method = mm.group(1) if mm else "GET" name = "(module)" for start, fname in func_starts: - if start <= cm.start(): + if start <= hm.start(): name = fname else: break - line = text.count("\n", 0, cm.start()) + 1 + line = text.count("\n", 0, hm.start()) + 1 calls.append((method, path, name, file, line)) return calls