إعادة هيكلة الشفرة بنيويًا
يبيّن هذا الدليل كيف تجعل وكيلًا يجري تغييرًا ميكانيكيًا واسعًا — سكربتًا لتحويل الشفرة، أو ترحيل 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() — وهذا غالبًا الخلل الذي كنت تبحث عنه. ولا بحث نصي يستطيع التعبير عن ذلك.
حين لا يجد النمط شيئًا
قسم بعنوان “حين لا يجد النمط شيئًا”جرّب شيئين، بهذا الترتيب:
- صغّره.
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")تحقق من عملك بالمُصرِّف
قسم بعنوان “تحقق من عملك بالمُصرِّف”المغزى من إنجاز هذا بنيويًا أنه واسع. والتغيير الواسع هو حيث تكسر إعادة كتابة تبدو سليمة أحد الأنواع. اطلب التشخيصات بعد استدعاء 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 |
انظر أيضًا
قسم بعنوان “انظر أيضًا”- ذكاء الشفرة: لماذا المطابقة البنيوية ليست تعبيرًا نمطيًا بخطوات إضافية
- أدوات الوكيل المضمّنة: القائمة الكاملة للوسائط
- البحث في الشفرة عبر مخطط الشفرة: جانب الرموز والتأثير