רפקטורינג מבני של קוד
מדריך זה מראה כיצד לגרום לסוכן לבצע שינוי מכני רחב — שינוי קוד אוטומטי, מעבר 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() — וזה בדרך כלל הבאג שחיפשתם. שום חיפוש טקסטואלי לא יכול לבטא זאת.
כשהתבנית לא מוצאת דבר
קטע בשם “כשהתבנית לא מוצאת דבר”שני דברים לנסות, לפי הסדר:
- הקטינו אותה.
foo($X)לפניif (a) { foo($X); }. תבנית שאינה קוד תקף בפני עצמה לרוב אינה המבנה שהתכוונתם אליו. - ציינו את השפה.
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 |
נושאים קשורים
קטע בשם “נושאים קשורים”- אינטליגנציית קוד — למה התאמה מבנית אינה ביטוי רגולרי עם כמה שלבים נוספים
- כלי סוכן מובנים — רשימת הארגומנטים המלאה
- חיפוש קוד עם גרף הקוד — צד הסמלים וההשפעה