# -*- coding: utf-8 -*- '''防回归:抓"定义了但从没被调用"的方法。 为什么值得单独一个测试: 1.7.0 交付后用户装上发现**一片白屏**。根因是 private void initTabs() { ... } // 定义在第 937 行 而 onCreate 里只调了 buildControlPanel(),**从头到尾没人调 initTabs()**。 结果 tabDefs 为 null -> selectTab() 直接 return -> 内容区空白。 这种错误编译能过、别的测试也能过,只有真机跑起来才看得见, 所以必须静态查出来。 判定方法: 统计方法名在文件里出现的次数(定义那一处也算)。 只出现 1 次 = 只有定义没调用。 分两档: * **硬性失败**:关键路径上的方法(initTabs / 三个面板 / selectTab)。 这些漏了就是白屏或功能全无,必须拦住。 * **仅报告**:其它可疑方法。有些确实是历史遗留的死代码 (下面的 KNOWN_DEAD 记录了已确认的),有些是接口回调 (由框架调用,本文件里当然只有定义)。 报告出来供清理,但不拦构建 —— 免得噪音太大没人看。 ''' from __future__ import annotations import re import sys from pathlib import Path ROOT = Path(__file__).resolve().parent.parent MAIN = (ROOT / "android" / "src" / "com" / "lanremote" / "watch" / "MainActivity.java") # 框架/接口回调:由 Android 或 RemoteClient 调用,本文件里只有定义是正常的 WHITELIST = { "onCreate", "onDestroy", "onResume", "onPause", "onStart", "onStop", "onRestart", "onNewIntent", "onSaveInstanceState", "onRestoreInstanceState", "onBackPressed", "onKeyDown", "onKeyUp", "onWindowFocusChanged", "onRequestPermissionsResult", "onActivityResult", "onOptionsItemSelected", "onCreateOptionsMenu", "onPrepareOptionsMenu", "onConfigurationChanged", "onTouchEvent", "onClick", "onLongClick", "onTouch", "onAnimationUpdate", "onAnimationStart", "onAnimationEnd", "onAnimationCancel", "onAnimationRepeat", "onPreDraw", "onGlobalLayout", "onScrollChanged", "onItemClick", "onItemSelected", "onNothingSelected", "onFocusChange", "onEditorAction", "run", "call", "get", "set", "equals", "hashCode", "toString", "uncaughtException", "afterTextChanged", "beforeTextChanged", "onTextChanged", "onProgressChanged", "onStartTrackingTouch", "onStopTrackingTouch", "onDateSet", "onTimeSet", "onDismiss", "onCancel", "onShow", "onKey", "onCreateDialog", "onPrepareDialog", "onReceive", "onServiceConnected", "onServiceDisconnected", "onBind", "onCreateView", "onViewCreated", "onDestroyView", "onAttach", "onDetach", "onActivityCreated", "onOptionsMenuClosed", "onContextItemSelected", "onCreateContextMenu", "handleMessage", "onChildViewAdded", "onChildViewRemoved", "onGlobalFocusChanged", "onLayoutChange", "onScrollStateChanged", "onScrolled", "onPageStarted", "onPageFinished", "shouldOverrideUrlLoading", "onReceivedError", "onDownloadStart", "onChildDraw", # RemoteClient.Listener 的实现 "onDisconnected", "onEvent", "onFrame", "onProgress", "onLog", } # 已确认的历史遗留死代码:早期版本的实现,后来换成了别的写法, # 函数留着但没人调。不影响功能,列出来是为了有朝一日清掉。 KNOWN_DEAD = { "buildFeatureTab": "旧的分页构建方式,已被 renderGroups() 取代", "pickFolder": "已被 pickFile() 取代", "pickClipHistory": "功能已移到服务端注册表", "confirmWallpaper": "功能已移到服务端注册表", "confirmMonitor": "功能已移到服务端注册表", "sendMedia": "已被 media() 取代", "promptRun": "已被 runFeature() 取代", "showInfo": "已被 feature_run 的 info 取代", "findSavedToken": "旧的重连逻辑,已被 RelayTransport 取代", "glassBackground": "旧的美化辅助函数,样式改成 glass() 后没人用了", "sp": "旧的字号辅助,已被 dp() 取代", } # 关键路径:这些漏了就是白屏 / 功能全无,必须硬性拦住 CRITICAL = ("buildConnectPanel", "buildControlPanel", "initTabs", "buildMirrorPanel", "buildLanPanel") passed = 0 failed = 0 def check(name, ok, detail=""): global passed, failed if ok: passed += 1 print(" [PASS] " + name) else: failed += 1 print(" [FAIL] " + name + " " + str(detail)) def defined_methods(text: str) -> list[str]: """找出主程序里定义的方法名。 ⚠ **带 `@Override` 的跳过**。 为什么:这些是**父类/接口回调**,由框架调用,源码里当然只出现一次 —— 而这个扫描的判据正是"名字只出现 1 次 = 没人调"。 踩到的实例:手表端的 `GestureDetector.SimpleOnGestureListener` 里有 onSingleTapConfirmed / onDoubleTap / onScroll, 它们由 Android 的触摸框架回调,源码里各只出现一次。 不跳过的话扫描会把它们报成"未知的未调用方法"(假阳性)—— 而这种假阳性最坏的结果是**有人为了让它变绿,把回调删了**, 于是手势就失灵了。 同类还会遇到的:View.OnClickListener.onClick、 DialogInterface.OnClickListener.onClick、Runnable.run 等。 """ pattern = re.compile( r"(?:private|public|protected|static|final|\s)+" r"[\w<>\[\],.\s]+\s+(\w+)\s*\([^)]*\)\s*(?:throws [\w.,\s]+)?\{") names = [] for match in pattern.finditer(text): name = match.group(1) if name in ("if", "for", "while", "switch", "catch", "synchronized", "try", "return", "new"): continue # 往上看几行,如果有 @Override 就跳过 head = text[max(0, match.start() - 200):match.start()] tail_lines = head.splitlines()[-3:] if any("@Override" in ln for ln in tail_lines): continue names.append(name) return names def main() -> int: check("MainActivity.java 在", MAIN.is_file()) if not MAIN.is_file(): return 1 text = MAIN.read_text(encoding="utf-8") print() print("一、关键路径必须被调用(漏了就是白屏)") # initTabs 专项:白屏的根因就是它 init_def = len(re.findall(r"private void initTabs\s*\(", text)) init_use = len(re.findall(r"\binitTabs\s*\(\s*\)", text)) check("initTabs() 有定义", init_def >= 1, f"定义 {init_def} 处") check("initTabs() 被调用(否则连上后一片白屏)", init_use >= 2, f"出现 {init_use} 次(1 次是定义,>1 才算被调用)") check("initTabs() 紧跟 buildControlPanel()", re.search(r"buildControlPanel\(\);\s*(?://[^\n]*\n\s*)*initTabs\(\);", text) is not None, "必须在 buildControlPanel() 之后立刻调用") for method in CRITICAL: n = len(re.findall(r"\b" + method + r"\s*\(", text)) check(f"{method}() 被调用", n > 1, f"出现 {n} 次") print() print("二、分页定义完整性") tab_names = re.findall(r'new TabDef\("([^"]+)"', text) check("定义了 10 个分页", len(tab_names) == 10, f"实际 {len(tab_names)} 个") print(f" 分页: {' '.join(tab_names)}") check("selectTab 有定义", "private void selectTab(" in text) check("selectTab 被调用", len(re.findall(r"\bselectTab\s*\(", text)) > 1) print() print("三、未调用方法扫描") names = defined_methods(text) orphans = [] for name in sorted(set(names)): if name in WHITELIST or name in KNOWN_DEAD: continue n = len(re.findall(r"\b" + re.escape(name) + r"\s*\(", text)) if n <= 1: orphans.append((name, n)) check("没有**未知**的未调用方法", not orphans, "\n ".join(f"{n}({c} 次)" for n, c in orphans) if orphans else "") if orphans: print() print(" 未知的未调用方法(可能是漏接的事件处理,也可能是新的死代码):") for name, cnt in orphans: print(f" {name} (出现 {cnt} 次)") if KNOWN_DEAD: print() print(f" 已知死代码 {len(KNOWN_DEAD)} 个(不影响功能,待清理):") for name, why in sorted(KNOWN_DEAD.items()): still = len(re.findall(r"\b" + re.escape(name) + r"\s*\(", text)) mark = "" if still else " [已删除]" print(f" {name}: {why}{mark}") print() print("=" * 58) print(f"通过 {passed} 项,失败 {failed} 项") return 0 if failed == 0 else 1 if __name__ == "__main__": sys.exit(main())