تخطَّ إلى المحتوى

إعادة هيكلة الشفرة بنيويًا

يبيّن هذا الدليل كيف تجعل وكيلًا يجري تغييرًا ميكانيكيًا واسعًا — سكربتًا لتحويل الشفرة، أو ترحيل 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