情境
你想改 UserController.create 的回傳格式。問題是:這個函式被誰呼叫?改了之後哪些地方會跟著掛掉?
傳統做法是 grep -r "UserController" src/,然後一筆一筆過濾掉型別 import、註解、字串字面值,剩下真正「執行時會呼叫到」的地方。這個過程在大專案裡很花時間,而且容易漏掉透過動態分派或回呼(callback)進來的路徑。
CM 的 trace_path 工具能替你把這件事做完,而且一次給你完整的呼叫鏈。
前置作業
延續範例 009 的 todo-api 專案,假設圖譜已經建好。如果你還沒做這一步,先回到 009 跑一次 init 與 index。
步驟一:用 grep 看看會找到什麼
先用熟悉的方式起手:
grep -rn "UserController.create" src/
結果可能是:
src/controllers/UserController.ts:12: async create(req, res) {
src/routes/index.ts:8:router.post('/users', UserController.create);
tests/user.test.ts:24: const res = await UserController.create(req, fakeRes);
看起來三筆。第一筆是定義本身,第二筆是 route 綁定,第三筆是測試。表面上夠用,但有幾個風險:
- 如果有人寫
const create = UserController.create;然後再create(req),grep 找不到。 - 如果 route 用了 middleware 包一層,grep 看到的行號不一定是最終呼叫點。
- 註解或字串裡的
UserController.create會誤中。
步驟二:改用 trace_path
── 跟 AI 對話 ──
-
You:
列出所有會呼叫 UserController.create 的地方,我要看完整呼叫鏈。 -
AI:
我用
trace_path從UserController.create往上游追,方向是 inbound(被誰呼叫)。CM 呼叫:
trace_path(function_name: "UserController.create", direction: "inbound")CM 回傳(摘要):caller: src/routes/index.ts#POST /users (邊: HTTPRouteDependency) └─ calledBy: UserController.create @ src/controllers/UserController.ts:12 caller: tests/user.test.ts#24 (邊: test call) └─ calledBy: UserController.create caller: app/jobs/SyncUserWorker.ts#run (邊: CALLS) └─ calledBy: UserController.create (透過 const { create } = UserController 別名呼叫)分析: 除了 grep 看得到的 route 與測試之外,還有一個背景 Worker
SyncUserWorker.run透過解構賦值呼叫create。這正是 grep 容易漏掉的地方。
步驟三:grep 與 trace_path 對照
| 比較項目 | grep | trace_path |
|---|---|---|
| 找到的呼叫點 | 3 個(含 1 個定義) | 2 個真正呼叫者 + 1 個隱藏別名呼叫 |
| 別名/destructuring | ❌ 漏掉 const { create } = ... |
✅ 追得到 |
| 中間經過 middleware | ❌ 只看到綁定行 | ✅ 給邊的型別 (HTTPRouteDependency) |
| 註解誤中 | ✅ 容易 | ❌ 不會 |
| 看得到邊的型別 | ❌ | ✅ CALLS / test call / HTTPRouteDependency |
trace_path 給的不只是「檔名行號」,還有「這條呼叫是什麼性質」。route 綁定、測試呼叫、普通 CALLS,每條邊都帶型別標籤,讓你一眼分辨哪些是 production 路徑、哪些只是測試覆蓋。
步驟四:用結果規劃重構
從上面的清單,你的重構計畫會變成:
- 改
UserController.create本身:回傳新格式。 - 更新 route:
src/routes/index.ts不用改,但前端呼叫端要對應更新。 - 更新測試:
tests/user.test.ts的斷言要跟著改。 - 檢查 Worker:
SyncUserWorker.run是背景任務,確認它對新格式的相容性,這是 grep 容易遺漏、卻可能在 production 才爆開的點。
四個地方一次列完,不用反覆搜尋。
步驟五:呼叫鏈太深時調 depth
預設 trace_path 只展開一層呼叫者。如果你改的是底層函式,想看「呼叫者的呼叫者」這種第二層關係,把 depth 加大:
-
You:
我想看 UserController.create 的兩層呼叫鏈,找出誰「間接」會走到這個函式。 -
AI:
我把
depth設成 2,方向維持 inbound。CM 呼叫:
trace_path(function_name: "UserController.create", direction: "inbound", depth: 2)CM 回傳(摘要):[層 1] src/routes/index.ts#POST /users → UserController.create [層 2] └─ apps/web/src/api/users.ts#createUser → POST /users [層 2] └─ tests/integration/user.flow.test.ts → POST /users [層 1] app/jobs/SyncUserWorker.ts#run → UserController.create (別名呼叫) [層 2] └─ app/jobs/QueueConsumer.ts#handle → SyncUserWorker.run分析: 往上第二層看得到前端 API client
users.ts#createUser與 integration 測試。這些地方雖然不直接 importUserController,但只要create的回傳格式變了,前端 client 跟著要動。
depth 愈大,看到的間接呼叫者愈多,但也愈雜訊。實務上從 depth: 1 開始,看不夠再加,不要一次调到 5。
── inbound 與 outbound 的差別 ──
trace_path 的 direction 決定追的方向:
| 方向 | 意思 | 用在什麼時機 |
|---|---|---|
inbound |
誰呼叫我(上游) | 改函式前評估影響面 |
outbound |
我呼叫了誰(下游) | 看一個函式依賴哪些東西 |
both |
雙向都追 | 第一次理解某函式在系統裡的位置 |
重構前評估影響面,用 inbound;想理解某函式到底做了哪些事,用 outbound 把它的下游依賴展開。
關鍵學習點
trace_path是入門版影響分析:一個查詢列完所有呼叫者,方向inbound代表「我被誰呼叫」。- grep 會漏掉別名呼叫:
const { create } = UserController或動態分派,grep 找不到,trace_path可以。 - 每條邊帶型別:route 綁定、測試呼叫、普通 CALLS 分得清楚,規劃重構時不會把測試當 production。
- 重構前先跑一次:把
inbound清單當作 checklist,逐一處理,避免改 A 炸 B。下一篇範例 011 會示範如何用get_architecture從更高視角看整個專案。