extractors/ 語言抽取器

「設定 + 通用引擎」——支援 36 種文法的關鍵架構
檔案:graphify/extractors/(models 4KB · engine 260KB · resolution 120KB · 20+ 語言檔)

大方向

0.3.0 的重構把 12 份複製貼上的語言抽取器收斂成兩件事:一顆 LanguageConfig(設定)+ 一個通用引擎 _extract_generic()。語言差異(「函數節點長什麼樣」「import 怎麼寫」)變成設定;tree-sitter AST 走訪與節點/邊產生變成通用程式碼。這讓加一個語言從「複製 2000 行」變成「寫 100 行設定」。resolution.py 則負責最難的跨檔解析(tsconfig aliases、workspace packages、bare imports)。


1. models.py — 語言設定與符號事實(4KB,很小)

class LanguageConfig dataclass models.py:14「一個語言」的所有設定:節點型別、import 型別、特殊走訪掛勾。

這個 dataclass 是整個抽取架構的核心抽象。每個語言設定:

  • tree-sitter grammar 名稱與 node_types 對應(class / function / method 在該文法裡叫什麼)。
  • import / require 的節點型別與邊的關係名稱。
  • 可掛勾的語言特定回呼:型別收集(collect_type_refs)、額外走訪(extra_walk)、class 基底判斷、decorator 處理、scope 邊界…(這些 function 定義在 engine.py,例如 _python_collect_type_refs_csharp_extra_walk)。
  • synthesize_import_module_nodes 等 opt-in 旗標(0.8.40 加的 Swift import 修復)。

同檔的 _Symbol*Fact dataclasses(_SymbolDeclarationFact_SymbolImportFact_SymbolUseFact…)是解析器收集的「符號事實」,供 resolution 用。

閱讀建議:先看這個檔,再看 engine.py 的 _extract_generic——你就懂了整個架構。


2. engine.py — 通用抽取引擎(260KB,最大)

_extract_generic(path, config) Path, LanguageConfig → dict engine.py(由 extract.py 匯入)通用 tree-sitter 走訪:把 AST 變成 {nodes, edges}。

這是所有語言抽取器共用的心臟。它做的事(與 ARCHITECTURE.md 的「新增語言」步驟對應):

  1. 用 tree-sitter 把檔案解析成 AST(byte slice 一律 errors="replace" 解碼,非 UTF-8 也不當機)。
  2. 依照 config.node_types 走訪 class/function/method 節點,產生節點(ID 用 {file_stem}_{symbol} 形式,附 source_location: L{line})。
  3. 依照 config.import_types 找 import,產生 imports 邊。
  4. 收集 raw_calls(呼叫事件)供跨檔解析第二遍。
  5. 收集型別引用(collect_type_refs)供型別解析(uses 邊)。
  6. 呼叫語言特定的 extra_walk(如 C# 的 partial class 合併、Swift 的 extension 走訪)。

整個 260KB 塞滿了各語言的 _*_collect_type_refs_*_extra_walk、scope 計算、裝飾器、generator、async 等特殊語法的處理——這是「36 種文法」真正的深度所在。

_python_collect_type_refs · _csharp_extra_walk · _ts_extra_walk … engine.py 各處語言特定的掛勾函數家族。

段落式說明:每個語言在 engine.py 有一組 _<lang>_* 函數,解決「這個語言怎麼解析型別/作用域/特殊語法」:

  • _python_collect_type_refs — Python 型別註解收集;_PYTHON_ANNOTATION_NOISE 過濾 str/int/MagicMock 等內建型別(0.8.33 的 god-node 修復)。
  • _csharp_extra_walk / _csharp_partial 系列 — C# partial class 合併、attribute 解析。
  • _ts_extra_walk / _ts_decorator_name / _ts_receiver_type_table — TypeScript 裝飾器、receiver 型別表。
  • _swift_pre_scan / _swift_receiver_name — Swift extension 與 receiver 解析。
  • _js_extra_walk / _js_member_assignment_target — JavaScript 成員指派。

這組函數的命名規律(_<lang>_<用途>)本身就是一個好索引——看到名字就知道是哪個語言、做什麼。


3. resolution.py — 跨檔解析(120KB)

_read_tsconfig_aliases · _load_workspace_packages · _resolve_export_target … resolution.py 各處把「字串 import」解析成「實際檔案」——最難的解析邏輯。

跨檔解析的難題:import { Foo } from "@/lib/utils" 這個 @/lib/utils 到底是哪個檔案?resolution.py 負責:

  • tsconfig paths / baseUrl_read_tsconfig_aliases_resolve_tsconfig_alias 讀 tsconfig 的 aliases,把 @/… 對到實際路徑。
  • workspace 套件_find_workspace_root_pnpm_workspace_globs_load_workspace_packages——monorepo 的 pnpm/yarn workspace 與 package.json exports 欄位解析。
  • JS 路徑解析_resolve_js_import_path 處理副檔名省略、目錄 index、.js.ts 轉換。

這 120KB 是「讓 calls/imports 邊跨檔對得準」的真相所在——也是整個圖能不能跨檔連起來的關鍵。


4. 語言抽取器一覽

語言檔案家族(bash / go / rust / sql / terraform / pascal …) extractors/每個語言一個檔,做「設定 + 通用引擎」以外的專屬處理。

段落式說明:

  • sql.py(20KB)— SQL AST 抽取(tables、views、FK、reads_from);[sql] extra 提供 tree-sitter-sql。
  • go.py / rust.py / csharp.py / dart.py — 主要語言,各自處理特有的 import/module 系統。
  • bash.py(22KB)— shell 腳本(source/#include 邊)。
  • pascal.py + pascal_forms.py — Pascal/Delphi([pascal] extra)。
  • terraform.py — HCL([terraform] extra)。
  • apex.py — Salesforce Apex(regex-based)。
  • sln.py — Visual Studio solution 檔(專案參考)。
  • json_config.py / markdown.py — 特殊格式。
  • base.py(4KB)— extractor 基底類別。

還有 MIGRATION.md(5KB)記錄舊架構 → 新架構的遷移。

看完這頁你應該能說出:LanguageConfig 是什麼、加一個語言要改哪裡、tsconfig alias 在 resolution.py 怎麼處理。配合 pipelineextract_python 看,整條「設定 → 引擎 → 解析」就完整了。

① 進階真實情境 Worked Example:為一個冷門語言加入完整支援

前面是「看懂 LanguageConfig + 引擎」;這次是真的加一種語言:你的公司有大量 Kotlin 舊 codebase,graphify 不支援 Kotlin,你要把它加進抽取支援並通過測試。

  1. 寫 LanguageConfig:對照 models.py 的 dataclass,定義 Kotlin 的 class/function/method 對應的 tree-sitter 節點型別、import 型別、特殊掛勾——能塞進設定就別動 engine。
  2. 處理語言特例:Kotlin 的 extension functions、data class、`import` 的 package 形式——檢查是否需要 extra_walk 掛勾(如 _kotlin_extra_walk),非必要的話先做最小版本。
  3. 註冊三處_DISPATCH(extract.py 或 registry)、CODE_EXTENSIONS(detect.py)、_WATCHED_EXTENSIONS(watch.py);pyproject 加 tree-sitter-kotlin。
  4. 加 fixture 與測試tests/fixtures/sample.kt + test_languages.py 的測例——驗證 class/function/import 都抽得到,跨檔呼叫解析(Phase 2)也驗證一次。
  5. 驗證不破其他語言pytest tests/ -q 全跑——fixtures 是 30+ 語言共用的,新增不能弄壞別人。
為什麼選這條路徑:「設定 + 通用引擎」的架構讓新語言通常只需「設定 + 掛勾 + 註冊 + 測試」——這是 0.3.0 重構(2527 → 1588 行)的紅利。但別誤會成「零成本」:跨檔解析(resolution.py 的 tsconfig/workspace/JS 路徑)是另一層深度,Kotlin 的 package 解析若不做,import 邊就停在檔案級。先做「能用的最小版」再補 resolution,是務實的貢獻順序。

② 深入原理擴充:dispatch 的歧義處理,與「無抽取器也是一種答案」

extractors 頁最容易被忽略的是「決定不抽也是一種決定」:

大家以為建圖正確、但其實有誤的案例:以為「抽取器存在 = 這個語言的圖完整正確」。實際上每種語言的抽取深度不等:Python 有 rationale(docstring)節點、SQL 有表格/FK/JOIN 的確定性抽取、TS 有 tsconfig alias 解析——但 Bash 的 source 邊、Pascal 的 cross-file、Apex 的 regex 抽取都比較淺。最常見的誤判:看到 extract_kotlin 或任何新加的語言就以為「跨檔 calls 全通了」,其實該語言可能只做完「結構抽取」。判讀深度看 test_languages.py 有沒有該語言的 cross-file 測例,別看有沒有抽取器。

③ 診斷式疑難排解表

症狀可能原因解決方案
新語言節點都抽到了,但跨檔呼叫斷掉只有結構抽取,沒做該語言的 import/resolution 解析_import_<lang> 與 resolution 支援;先確認 Phase 2 是否涵蓋該語言
.h 檔被抽錯語言內容 sniff 誤判(C++ header 被當 C)檢查 _is_cpp_header 的判據;調整或改為保守(寧可警告)
`import { Foo } from "@/lib"` 解不開tsconfig alias 沒被讀到(baseUrl/paths 設定位置不同)確認 _read_tsconfig_aliases 有讀到該 tsconfig;檢查 resolve 的根目錄
新語言要裝的 grammar 裝了卻 ImportErrorpyproject 只加了 dependency 但沒加 optional extra 對映確認 extra(如 [sql])與 import 路徑一致;pip install "graphify[<lang>]"
加語言後別的語言測試爆炸registry 全域註冊污染、或 fixture 命名撞檔(如 sample.f90 vs .F90)pytest tests/ -q 對比爆炸範圍;檢查 registry 是否有共享可變狀態

④ 進階挑戰題

  1. 「無抽取器寧可警告也不硬解析」的原則(.m 檔)犧牲了「能抽到一點」。請設計一個決策規則:什麼情況下「partial extraction(抽出確定無誤的部分)」優於「零抽取 + 警告」?
  2. LanguageConfig 想把語言差異全部參數化。請找出至少兩個「無法被參數化、必須靠 engine 新 code」的語言特性(例如 C# partial class 合併),並解釋為何無法表達成設定。
  3. resolution.py 目前支援 tsconfig/workspace/JS 路徑。請評估是否該把「Python 的 __init__.py 重新匯出、Go 的 module 路徑」也納入 resolution——納入的話,Phase 2 的跨檔邊會變多還是變準?各自的風險是什麼?