ข้ามไปยังเนื้อหา

รีแฟกเตอร์โค้ดเชิงโครงสร้าง

คู่มือนี้แสดงวิธีทำให้เอเจนต์ลงมือเปลี่ยนแปลงแบบกลไกในวงกว้าง — โค้ดมอด การโยกย้าย API แบบยกชุด แพตเทิร์นที่คุณอยากให้หายไป — ด้วยการค้นหาและเขียนใหม่เชิงโครงสร้าง โดยพักการเปลี่ยนแปลงไว้ให้รีวิวก่อนที่อะไรจะถูกเขียนลงไป

ast_grep จับคู่โค้ดตามรูปร่าง ast_edit เขียนมันใหม่ resolve คอมมิตหรือทิ้ง

เครื่องมือเชิงโครงสร้างครอบคลุมภาษาที่กราฟโค้ดแยกวิเคราะห์: Dart, JavaScript, TypeScript, TSX, PHP, Python, Rust, Zig, C, C++, Go, Java, Ruby, C#, Swift, Kotlin, R, Assembly, MATLAB และ Ada เครื่องมือเหล่านี้ต้องมีไวยากรณ์ tree-sitter เตรียมไว้พร้อม ซึ่งในบิลด์ที่จัดส่งมาก็มีเสมอ — ไวยากรณ์ที่หายไปจะทำให้เซิร์ฟเวอร์หยุด ไม่ใช่ทำให้การค้นหาด้อยลง

อย่าเขียนอะไรที่คุณยังไม่ได้ดูมันก่อน ขอให้เอเจนต์ค้นหา:

หาทุกจุดที่เราเรียก dispose() บนค่าที่เป็น null ได้ ซึ่งเราเพิ่งตรวจว่าไม่เป็น null

ast_grep(pattern: "if ($X != null) $X.dispose()", language: "dart")

ผลลัพธ์คือ file:line พร้อมซอร์สที่จับคู่ได้ของแต่ละจุด

โทเคน จับคู่กับ
$NAME โหนดเดียวใด ๆ และถูกจับเก็บไว้
$$$NAME โหนดพี่น้องที่ต่อเนื่องกันชุดใดก็ได้ — อาร์กิวเมนต์ คำสั่ง
$_ อะไรก็ได้ และถูกจับเก็บโดยไม่มีชื่อ

ข้อที่สำคัญคือ $NAME ที่ซ้ำกันต้องจับข้อความเดียวกัน

if ($X != null) $X.dispose()

หาโค้ดป้องกันที่ dispose สิ่งที่มันเพิ่งตรวจสอบเจอ แต่จะไม่หา if (a != null) b.dispose() เจอ — ซึ่งปกติแล้วคือบั๊กที่คุณกำลังตามหาอยู่พอดี ไม่มีการค้นหาเชิงข้อความใดที่แสดงออกมาแบบนั้นได้

สองอย่างที่ลองได้ เรียงตามลำดับ:

  1. ทำมันให้เล็กลง ใช้ foo($X) ก่อน if (a) { foo($X); } แพตเทิร์นที่ไม่ใช่โค้ดที่ถูกต้องในตัวเองมักไม่ใช่โครงสร้างที่คุณตั้งใจ
  2. ระบุชื่อภาษา language: จะถูกอนุมานก็ต่อเมื่อ path ชี้ไปที่ไฟล์เดียว

แพตเทิร์นที่ไม่มีบริบทการแยกวิเคราะห์ใดยอมรับ จะถูกรายงานว่าแยกวิเคราะห์ไม่ได้ — เครื่องมือจะบอกเช่นนั้น แทนที่จะรายงานว่า “ไม่พบรายการที่ตรงกัน” สำหรับสิ่งที่ไม่เคยถูกอ่านในแบบที่มันดูเหมือนเลย

ast_edit(
pattern: "if ($X != null) $X.dispose()",
rewrite: "$X?.dispose()",
language: "dart",
)

ผลลัพธ์คือข้อเสนอ:

(proposed) ast_edit: if ($X != null) $X.dispose() → $X?.dispose()
12 replacements in 4 files:
lib/features/a/x.dart (3)
lib/features/b/y.dart (5)
…
Nothing has been written. Call resolve with edit_id "edit_1" and action
"accept" to apply it, or "discard" to drop it.

ยังไม่มีอะไรอยู่บนดิสก์ อ่านรายการ ตรวจดูสักสองสามจุดที่การค้นหารายงาน แล้ว:

resolve(edit_id: "edit_1", action: "accept")

หรือ

resolve(edit_id: "edit_1", action: "discard")

ประเด็นของการทำแบบเชิงโครงสร้างคือมันกว้าง และการเปลี่ยนแปลงที่กว้างก็คือจุดที่การเขียนใหม่ที่ดูสมเหตุสมผลทำไทป์พังพอดี ขอผลวินิจฉัยหลัง resolve:

lsp(action: "diagnostics", file: "*")

file: "*" รันตัวตรวจสอบของโปรเจกต์เอง (dart analyze, tsc --noEmit, cargo check, go build) แทนที่จะเป็นการค้นต่อไฟล์ — เป็นรูปคำถามที่ถูกต้องหลังการเปลี่ยนแปลงที่แตะไปสี่สิบไฟล์

สมมติว่า log(msg, level) กำลังกลายเป็น log(level: level, message: msg)

ast_grep(pattern: "log($MSG, $LEVEL)", language: "dart")

ดูผลที่เจอ ถ้าบางอันเป็น log แบบสองอาร์กิวเมนต์ที่ต่างกัน ให้ทำให้แพตเทิร์นแคบลงโดยจำกัด path ไว้ที่ไดเรกทอรีที่เป็นเจ้าของตัวที่คุณหมายถึง แทนที่จะพยายามอธิบายความต่างนั้นในแพตเทิร์น

ast_edit(
pattern: "log($MSG, $LEVEL)",
rewrite: "log(level: $LEVEL, message: $MSG)",
path: "lib/features/telemetry",
language: "dart",
)

จากนั้น resolve แล้วต่อด้วย lsp(action: "diagnostics", file: "*")

สถานการณ์ เครื่องมือที่เหมาะกว่า
การเปลี่ยนชื่อสัญลักษณ์ lsp_rename — มันตามการอ้างอิง ไม่ใช่รูปร่าง
การหาสตริง คอมเมนต์ หรือชื่อไฟล์ search (เนื้อหา) / search_files (ชื่อ)
“ถ้าฉันเปลี่ยนตรงนี้ อะไรจะพัง” กราฟโค้ด
ภาษาที่ไวยากรณ์ยังไม่ครอบคลุม search บวกกับ edit