|
|
|
@ -46,17 +46,58 @@ def load_spec(): |
|
|
|
return ops |
|
|
|
|
|
|
|
|
|
|
|
def _skip_generics(text: str, i: int) -> int: |
|
|
|
"""从 text[i] 开始(i 指向 '<')跳过一层平衡尖括号,返回 '>' 之后的位置。 |
|
|
|
|
|
|
|
⚠️ 不能简单用正则 `<[^>]*>`:泛型里常嵌套 `>`,例如 |
|
|
|
request<{ items: Omit<ApplicationItem, 'id'>[] }>('/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<q>['\"`])(?P<path>[^'\"`]+)(?P=q)", |
|
|
|
re.DOTALL, |
|
|
|
) |
|
|
|
head_re = re.compile(r"\brequest\b") |
|
|
|
arg_re = re.compile(r"\s*(?P<q>['\"`])(?P<path>[^'\"`]+)(?P=q)") |
|
|
|
func_re = re.compile( |
|
|
|
r"^\s*(?:export\s+)?(?:async\s+)?function\s+(?P<name>\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 |
|
|
|
|
|
|
|
|