Bỏ qua để đến nội dung

Tái cấu trúc mã theo cấu trúc

Hướng dẫn này chỉ cho bạn cách để một agent thực hiện một thay đổi cơ học trên diện rộng — một codemod, một đợt di trú API hàng loạt, một mẫu bạn muốn loại bỏ — bằng tìm kiếm và viết lại theo cấu trúc, với thay đổi được đưa vào vùng chờ để xem xét trước khi bất cứ thứ gì được ghi.

ast_grep khớp mã theo cấu trúc. ast_edit viết lại nó. resolve áp dụng hoặc hủy bỏ.

Các công cụ cấu trúc bao phủ những ngôn ngữ mà đồ thị mã phân tích cú pháp: Dart, JavaScript, TypeScript, TSX, PHP, Python, Rust, Zig, C, C++, Go, Java, Ruby, C#, Swift, Kotlin, R, Assembly, MATLAB và Ada. Chúng cần các ngữ pháp tree-sitter được chuẩn bị sẵn, và trong một bản dựng phát hành thì chúng luôn có sẵn — một ngữ pháp thiếu sẽ chặn máy chủ chứ không làm suy giảm tìm kiếm.

Đừng bao giờ viết lại thứ bạn chưa xem qua. Hãy yêu cầu agent tìm kiếm:

Tìm mọi chỗ chúng ta gọi dispose() trên một giá trị nullable mà chúng ta vừa kiểm tra null.

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

Kết quả là file:line kèm mã nguồn khớp được ở mỗi vị trí.

Token Khớp
$NAME Một nút bất kỳ, được bắt lại
$$$NAME Một dãy nút anh em bất kỳ — đối số, câu lệnh
$_ Bất cứ thứ gì, bắt lại nhưng không đặt tên

Điều quan trọng nhất: một $NAME lặp lại phải bắt đúng cùng một đoạn văn bản.

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

sẽ tìm thấy một câu lệnh bảo vệ giải phóng đúng thứ nó vừa kiểm tra. Nó không tìm thấy if (a != null) b.dispose() — mà đó thường chính là lỗi bạn đang tìm. Không tìm kiếm văn bản nào diễn đạt được điều đó.

Hai điều cần thử, theo thứ tự:

  1. Thu nhỏ nó lại. Dùng foo($X) trước if (a) { foo($X); }. Một mẫu không phải là mã hợp lệ khi đứng một mình thường cũng không phải là cấu trúc bạn muốn.
  2. Nêu rõ ngôn ngữ. language: chỉ được suy ra khi path trỏ tới một tệp duy nhất.

Một mẫu mà không ngữ cảnh phân tích nào chấp nhận sẽ được báo là không thể phân tích cú pháp — công cụ nói rõ điều đó thay vì báo “không có kết quả khớp” cho một thứ chưa từng được đọc theo cách nó trông như vậy.

Viết lại, và xem xét trước khi áp dụng

Phần tiêu đề “Viết lại, và xem xét trước khi áp dụng”
ast_edit(
pattern: "if ($X != null) $X.dispose()",
rewrite: "$X?.dispose()",
language: "dart",
)

Kết quả là một đề xuất:

(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.

Chưa có gì được ghi lên đĩa. Hãy đọc danh sách, kiểm tra một vài vị trí mà tìm kiếm đã báo, rồi:

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

hoặc

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

Lý do làm việc này theo cấu trúc là vì nó có phạm vi rộng. Những thay đổi rộng chính là nơi một bản viết lại trông có vẻ hợp lý sẽ làm vỡ một kiểu dữ liệu. Hãy yêu cầu chẩn đoán sau khi resolve:

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

file: "*" chạy trình kiểm tra của chính dự án (dart analyze, tsc --noEmit, cargo check, go build) thay vì truy vấn từng tệp — đúng kiểu câu hỏi cần đặt ra sau một thay đổi đụng tới bốn mươi tệp.

Giả sử log(msg, level) đang chuyển thành log(level: level, message: msg).

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

Hãy xem các kết quả khớp. Nếu một số trong đó là một log hai đối số khác, hãy thu hẹp mẫu bằng cách giới hạn path vào thư mục chứa hàm bạn muốn, thay vì cố diễn đạt sự khác biệt đó trong mẫu.

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

Sau đó resolve, rồi lsp(action: "diagnostics", file: "*").

Tình huống Công cụ phù hợp hơn
Đổi tên một ký hiệu lsp_rename — nó đi theo tham chiếu, không theo cấu trúc
Tìm một chuỗi, một chú thích hay một tên tệp search (nội dung) / search_files (tên)
“Thay đổi này sẽ làm hỏng những gì?” Đồ thị mã
Một ngôn ngữ mà các ngữ pháp không bao phủ search kèm edit