程式碼智慧
Control Center 讓代理有四種閱讀以外的方式來理解程式碼。它們回答的是截然不同的問題;之所以是四種而不是一種,是因為沒有任何單一機制能回答全部的問題。
| 機制 | 回答的問題 | 成本 |
|---|---|---|
| 程式碼圖 | 這裡有什麼,以及什麼依賴它 | 一次索引處理;查詢成本低 |
| 語言伺服器 | 這真的能編譯嗎,而這是什麼型別 | 每個專案一個伺服器行程 |
| 結構化搜尋 | 這個「形狀」出現在哪裡 | 每個檔案一次剖析 |
| 除錯工具 | 在這個時刻、這個堆疊框架裡,什麼是真的 | 一個停止的行程 |
語言伺服器補上的缺口
標題為「語言伺服器補上的缺口」的章節程式碼圖是從 tree-sitter 建構出來的:符號、呼叫邊、影響半徑。它很快、很省,而且對型別一無所知。它沒辦法告訴你,你剛寫的呼叫能不能編譯、那個欄位是否存在於物件上,或是你的重新命名有沒有漏掉某個再匯出。
沒有語言伺服器,代理要等到測試時才會發現——或者永遠不會發現。它寫出看起來合理的東西、繼續往下走,而失敗會在三個回合後、掛在另一個變更上浮現。
所以真正的收穫其實不是 lsp 工具,而是 write、edit 和 apply_patch 都被包了起來:每次寫入之後,檔案都會經過語言伺服器,新產生的診斷會併入工具結果。代理會在建置壞掉的那一刻就知道自己弄壞了它,無論它有沒有想到要問。
只回報新的診斷
標題為「只回報新的診斷」的章節重複代理已經被告知過的內容,比什麼都不說還糟:它會用雜訊塞滿結果,代理會學會略過這些雜訊,而那唯一的新錯誤也跟著被略過。每次執行都有一份帳本記錄已回報過的項目,並以嚴重性、代碼和訊息為鍵——不是以位置為鍵——所以只是往下移了四行的診斷,不會被當成新問題重新回報。
延後啟動,並誠實面對自己的缺席
標題為「延後啟動,並誠實面對自己的缺席」的章節開機時不會啟動任何伺服器。它會在第一次有需求時啟動,而那幾乎總是某個它宣稱支援的檔案第一次被編輯時;閒置的伺服器會被清掉。從不碰 Dart 的主機永遠不會啟動分析器。
偵測是一種交集:簽出的內容裡必須存在根標記,而且執行檔必須能解析得到。只有標記,只代表「這個專案是 Python」,完全沒說伺服器裝了沒;只有能解析的執行檔,只代表這台主機有工具鏈,完全沒說這個簽出是否有關。唯有兩者同時成立,查詢才可能成功;一個提供了卻總是失敗的工具,比根本不存在的工具還糟。
既然有 grep,為什麼還需要結構化搜尋
標題為「既然有 grep,為什麼還需要結構化搜尋」的章節grep 比對的是字元。找名稱時它是最合適的工具,找形狀時則完全不對。
結構化的模式是用和檔案相同的文法剖析的,所以 dispose($X) 找得到那個呼叫,不管它怎麼排版、換行或斷行;它也永遠不會比對到字串常值或註解裡相同的字元。
讓它不只是「多加幾步的 regex」的關鍵性質是:同一個模式中重複出現的中繼變數,必須補抓到相同的文字:
if ($X != null) $X.dispose()會比對到「處置它剛剛檢查過的對象」的防護,而不會比對到處置其他東西的防護。這是一整類用文字表達不出來的 bug。
它跑在已經連結進來的 tree-sitter 上
標題為「它跑在已經連結進來的 tree-sitter 上」的章節沒有第二個引擎。cc_natives 為了程式碼圖,本來就會載入 tree-sitter 及其文法,並帶有已編譯查詢的快取和語言表。比對器是純 Dart,跑在原本就已經在剖析的樹上。再搞一個結構化引擎,就代表要有第二個剖析器、第二組得在每個平台上建置和部署的文法,以及對「什麼是呼叫運算式」的兩種答案。
片段需要有地方可以落腳
標題為「片段需要有地方可以落腳」的章節文法只定義了整個檔案是什麼。在 Dart 最上層單獨剖析,dispose(x) 是完全合法的函式簽章——一個名為 dispose、接受參數 x 的宣告——不會有任何錯誤洩漏它的身分。用那份剖析結果打造的比對器會去找宣告,找不到呼叫,然後對一個從來就不是以它看起來的樣子被讀取的模式,回報「沒有相符項目」。
因此,模式會依序放進一連串鷹架裡試——陳述式、運算式、類別成員、最上層——第一個接受的勝出。沒有任何情境接受的片段,會被回報為無法剖析,而不是被搜尋後悄悄判定找不到。
為什麼重寫要先暫存,而不是直接寫入
標題為「為什麼重寫要先暫存,而不是直接寫入」的章節結構化重寫是代理能做的最廣泛編輯:一個模式、四十個檔案,而且沒有人會逐行讀結果——模型不會,提出要求的人也不會。直接寫入並回報「完成」,等於這件事第一次被認真檢視,是之後的 git diff。
所以 ast_edit 回報的是它將會做什麼——數量、檔案、相符的位置——只有當 resolve 提交時,變更才會落地。這也正是能讓一份差異和「接受/捨棄」在任何東西寫到磁碟前,先擺在人面前的原因。
提交是全有或全無,而且每個檔案都會拿來和暫存當下捕捉到的內容比對。在暫存與提交之間,代理可能手動編輯過其中一個檔案、診斷流程可能重寫過它,或格式化工具可能動過它——硬是提交會丟掉那些工作,而且差異看起來還會像是刻意為之。只要有任何不符,整份變更都會被拒絕,因為只套用一半的結構化重寫,會留下一棵在新舊兩種形狀下都編譯不過的樹。
除錯工具能回答哪些列印陳述式答不出的問題
標題為「除錯工具能回答哪些列印陳述式答不出的問題」的章節列印陳述式的迴圈是:加一行列印、把整個東西重跑一遍、讀輸出、刪掉列印。每個問題都要付上一整輪測試執行的代價,而且它只回答你在執行之前就想到要問的問題。
停止的堆疊框架能一次回答關於那個時刻的每個問題——每個區域變數、每個呼叫者,以及在該框架自身範圍內求值的運算式。對於卡死的行程,或十次執行才重現一次的失敗,這就是花一個下午和花一分鐘的差別。
像隔離艙一樣有界,理由也相同
標題為「像隔離艙一樣有界,理由也相同」的章節除錯配接器擁有一個停止的行程,並持有那個行程所持有的一切——連接埠、鎖定、資料庫連線。沒有人操作的工作階段,是一種活得比啟動它的那個回合還久的洩漏。所以有硬性 TTL、每個對話恰好一個工作階段,而且第二次 launch 會被拒絕,而不是默默取代第一個:一個沒結束第一個就啟動第二個的代理,幾乎可以確定已經搞丟了第一個。
一套傳輸,兩種協定
標題為「一套傳輸,兩種協定」的章節Debug Adapter Protocol 和 Language Server Protocol 共用 Content-Length 分幀,而在它之上什麼都不共用——不同的關聯鍵、不同的失敗形狀,還多了第三種訊息型別(事件),在 LSP 裡沒有對應。分幀只存在於一個地方;兩種協定各自疊在它上面。
把分幀寫兩次,就代表「一則訊息橫跨兩個區塊」、「一個區塊裡有三則訊息」和「伺服器在送出第一個訊框前先往 stdout 寫了橫幅」都會有兩種答案,而只有其中一個會得到修正。
相關內容
標題為「相關內容」的章節- 用程式碼圖搜尋程式碼——符號與影響範圍那一面
- 用代理除錯失敗的測試——除錯工具的端到端流程
- 以結構方式重構程式碼——
ast_grep與ast_edit - 內建代理工具——每個操作與引數