דלגו לתוכן

רפקטורינג מבני של קוד

מדריך זה מראה כיצד לגרום לסוכן לבצע שינוי מכני רחב — שינוי קוד אוטומטי, מעבר 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() על ערך nullable שרק בדקנו שאינו null.

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

הפלט הוא file:line בתוספת קוד המקור המתאים לכל אתר.

מה נותן לכם תחביר התבנית

קטע בשם “מה נותן לכם תחביר התבנית”
אסימון מתאים ל
$NAME צומת בודד כלשהו, שנלכד
$$$NAME כל רצף של אחים — ארגומנטים, פקודות
$_ כל דבר, בלי ללכוד אותו בשם

הנקודה שקובעת: $NAME שמופיע שוב בתבנית חייב ללכוד את אותו טקסט.

if ($X != null) $X.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")

בדקו את העבודה שלכם עם המהדר

קטע בשם “בדקו את העבודה שלכם עם המהדר”

הטעם שבעשייה מבנית הוא שהשינוי רחב. שינויים רחבים הם בדיוק המקום שבו שכתוב שנראה סביר שובר טיפוס. בקשו אבחונים אחרי ההחלה:

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