مطالبات لمراجعة الكود: 16 فحصًا جاهزًا
مراجعة الكود بالذكاء الاصطناعي قبل دمجه: ستة عشر مقطعًا يحمل كل منها عيبًا حقيقيًا، ومعه السؤال الذي يطرحه المراجع عليه. كل بطاقة تضبط مسبقًا اللغة والتركيز والبيئة — استبدل المثال بكودك واضغط تطبيق.
الأخطاء والمنطق
كود يُترجم بنجاح ومع ذلك يكذب: البحث عن أخطاء منطقية في الكود، ووسيط افتراضي قابل للتغيير يحمل بضاعة غيره، وصفر يُفهم غيابًا، ووقت بلا منطقة زمنية، وانحدار يتسلّل داخل طلب دمج.
وسيط افتراضي قابل للتغيير
الفخ الكلاسيكي في بايثون: القائمة في التوقيع تبقى حيّة بين الاستدعاءات وتجمع بيانات الآخرين.
راجع دالة السلة هذه: يشكو العملاء من العثور في طلبهم على منتجات لم يضيفوها.def add_item(item, cart=[]): cart.append(item) return cart
السطر 1 · حرج · خطأتُنشأ cart=[] مرة واحدة عند استيراد الوحدة، لا مع كل استدعاء: العميل الثاني يواصل ملء سلة العميل الأول إلى أن تُعاد تشغيل العملية.الإصلاح: cart: list | None = None، ثم في أول سطر من الجسم if cart is None: cart = [].
صفر يُفهم على أنه غياب
اختبار الصدق يخفي صفرًا مشروعًا، فيرى العميل ذو الرصيد الفارغ نصًا بديلًا.
انظر إلى هذا الفحص: هل يميّز فعلًا بين قيمة غائبة وصفر؟def render_balance(user): balance = user.get("balance") if not balance: return "لا توجد بيانات" return format_money(balance)
السطر 3 · جسيم · خطأتلتقط if not balance كلًا من None و0 و0.0 بالطريقة نفسها: من أنفق رصيده حتى آخر قرش يرى "لا توجد بيانات" بدل صفر صادق.الإصلاح: if balance is None — يُفحص الغياب صراحةً، أما الصفر فيمر إلى المنسّق كأي رقم آخر.
مقارنة وقت بلا منطقة زمنية
تعيد datetime.now() الوقت المحلي للخادم بينما تُحفظ صلاحية الرمز بتوقيت UTC.
تحقق من حساب صلاحية الرمز. الخدمة تعمل على خوادم في مناطق زمنية مختلفة.def is_expired(token): return datetime.now() > token.expires_at
السطر 2 · حرج · خطأتعطي datetime.now() وقتًا محليًا بلا منطقة، بينما يأتي expires_at من قاعدة البيانات بتوقيت UTC: على خادم في دبي تعيش الرموز أربع ساعات زائدة، وأمام قيمة aware تسقط المقارنة فورًا بـ TypeError.الإصلاح: datetime.now(timezone.utc) وقاعدة واحدة للمشروع كله — لا يُحفظ إلا وقت aware بتوقيت UTC.
انحدار داخل طلب دمج
مراجعة للتغيير نفسه لا للملف: ما الذي يذهب إلى الإنتاج مع هذا الكوميت.
راجع هذا الـ diff من طلب دمج: ما الذي يذهب معه بالضبط إلى الإنتاج؟@@ -12,8 +12,7 @@ def apply_discount(order, percent): if order.paid: raise ValueError("order already paid")- if percent > 90:- raise ValueError("discount too large") order.total = order.total * (100 - percent) / 100+ order.discount_percent = percent
الأسطر 15-16 · حرج · خطأكان الفحص المحذوف هو الحد الوحيد على percent: عند 150 يصبح إجمالي الطلب سالبًا، فتحوّله الكاشير إلى عملية استرداد.الإصلاح: إعادة الفحص أو نقله إلى التحقق من الطلب، وإضافة الحالة percent=150 إلى الاختبارات.
الأمان
المواضع التي يصل فيها إدخال الغرباء إلى قاعدة البيانات ونظام الملفات: فحص ثغرات الأمان في الكود، وحقن SQL في حقل البحث، ومفتاح إنتاجي مكتوب داخل السكربت، وخروج من مجلد الرفع.
حقن SQL في البحث
يُلصق العنوان القادم من النموذج داخل نص SQL، فيصبح من ملأ النموذج هو من يتحكم بقاعدة البيانات.
راجع دالة البحث عن المستخدم هذه. البريد يأتي مباشرة من نموذج على الموقع.def find_user(conn, email): query = "SELECT * FROM users WHERE email = '" + email + "'" return conn.execute(query).fetchone()
السطر 2 · حرج · أمانيُلصق email داخل نص SQL: قيمة تحتوي على علامة اقتباس تُنهي الشرط، وكل ما بعدها تنفّذه قاعدة البيانات كأنه استعلامك أنت — بما في ذلك DROP TABLE.الإصلاح: معامل بدل اللصق — conn.execute("SELECT id, email FROM users WHERE email = %s", [email])؛ ومعه يختفي SELECT * الذي يجلب اليوم حتى تجزئة كلمة المرور.
رمز سري داخل السكربت
مفتاح إنتاجي مكتوب في المستودع، ويُطبع في الطريق داخل سجل النشر.
راجع سكربت النشر هذا: هل فيه ما هو خطر من ناحية الأمان؟#!/usr/bin/env bashAPI_TOKEN="sk_live_9f3c1ad84b22"echo "deploying with $API_TOKEN"curl -H "Authorization: Bearer $API_TOKEN" -X POST https://api.example.com/deploy
الأسطر 2-3 · حرج · أمانرمز إنتاجي مكتوب في ملف داخل git: يملكه في سجله كل من استنسخ المستودع يومًا، كما ينسخه echo إلى سجل النشر الذي يقرأه عدد أكبر بكثير.الإصلاح: قراءته من البيئة ($API_TOKEN بلا قيمة افتراضية)، وحذف echo، واعتبار المفتاح مسرّبًا — أي تدويره فورًا.
الخروج من مجلد الرفع
يُؤخذ اسم الملف من الطلب كما هو، ونقطتان وشرطة مائلة تقودان إلى أي ملف على الخادم.
راجع نقطة تنزيل الملفات هذه: اسم الملف يأتي من سلسلة الاستعلام.def download(request): name = request.args.get("file") path = os.path.join("/var/app/uploads", name) return send_file(path)
السطر 3 · حرج · أمانيخرج os.path.join من /var/app/uploads بلا اعتراض حين يكون الاسم ../../etc/passwd، أما المسار المطلق فيُلغي الوسيط الأول كليًا: يصبح أي ملف يقرؤه المسار قابلًا للتنزيل.الإصلاح: os.path.basename(name)، ثم مقارنة os.path.realpath للنتيجة بمجلد الرفع وتسليم الملف فقط إذا كان داخله.
الأداء
تحسين أداء الكود البطيء: استعلامات قاعدة بيانات داخل حلقة تتكرّر ألف مرة، واستعلام يتجاوز الفهرس فيقرأ الجدول كاملًا، وملف سجل يدخل الذاكرة دفعة واحدة حتى يمتلئ الخادم.
استعلامات داخل الحلقة
يذهب التقرير إلى قاعدة البيانات مرتين لكل مستخدم: ألف سطر تتحول إلى ألفي استعلام.
انظر إلى هذا التقرير: مع ألف مستخدم يستغرق دقيقة. أين يذهب الوقت؟def orders_report(user_ids): rows = [] for user_id in user_ids: user = db.query("SELECT name FROM users WHERE id = %s", user_id) orders = db.query("SELECT total FROM orders WHERE user_id = %s", user_id) rows.append((user.name, sum(o.total for o in orders))) return rows
الأسطر 3-5 · جسيم · أداءاستعلامان لكل مستخدم: ألف مستخدم يعني ألفي رحلة ذهاب وإياب عبر الشبكة، والوقت يضيع في الشبكة لا في الحساب.الإصلاح: استعلام واحد مع JOIN وGROUP BY users.id، أو استعلامان مع IN ودمج النتائج في الذاكرة.
استعلام يتجاوز الفهرس
دالة على العمود وعلامة نسبة في بداية LIKE تُطفئان الفهارس: تقرأ قاعدة البيانات الجدول كاملًا.
راجع هذا الاستعلام: على جدول فيه عشرة ملايين سطر يستغرق عشرين ثانية.SELECT *FROM ordersWHERE date_trunc('day', created_at) = '2026-09-01' AND lower(email) LIKE '%@example.com'ORDER BY created_at DESC
الأسطر 3-4 · جسيم · أداءتجعل date_trunc على created_at الفهرس عديم الفائدة، وLIKE التي تبدأ بعلامة نسبة لا تستطيع استخدامه أصلًا: يبقى مسح تسلسلي للجدول كله، كما يسحب SELECT * أعمدة لا يقرؤها أحد.الإصلاح: مقارنة created_at بمجال (من منتصف ليل 1 سبتمبر وأصغر من منتصف ليل اليوم التالي)، وفهرسة النطاق على حدة أو حفظه في عمود خاص، واختيار الحقول المستخدمة فقط.
السجل كله في الذاكرة
يُقرأ الملف دفعة واحدة، فيصبح حجم السجل هو حجم العملية نفسها.
راجع عدّاد الأخطاء هذا في ملف سجل. أحجام الملفات تبلغ عدة غيغابايت.def count_errors(path): lines = open(path).read().split("\n") return len([line for line in lines if "ERROR" in line])
السطر 2 · جسيم · أداءترفع read() الملف كاملًا إلى الذاكرة، ويضاعف split الاستهلاك، وتحتفظ القائمة داخل len() بنسخة ثالثة: مع سجل بحجم ثمانية غيغابايت يصل قاتل الذاكرة قبل النتيجة. كما أن الملف لا يُغلق أبدًا.الإصلاح: with open(path) as f ثم sum(1 for line in f if "ERROR" in line) — سطرًا سطرًا وباستهلاك ذاكرة ثابت.
قابلية القراءة والمعايير
ما يراه المراجع أولًا: تبسيط أربع عبارات if متداخلة، وأسماء متغيّرات من حرف واحد تخالف دليل الأسلوب، ونسبة ضريبة منسوخة في موضعين تتغيّر إحداهما وحدها يومًا ما.
أربعة شروط متداخلة
قاعدة إرسال الرسالة مختبئة في المستوى الخامس من الإزاحة ولا تُقرأ إلا دفعة واحدة.
قيّم قابلية قراءة هذه الدالة: لا بد من قراءتها حتى آخرها لمعرفة الشرط.def notify(user): if user is not None: if user.email is not None: if user.subscribed: if not user.banned: send_email(user.email) return True return False
الأسطر 2-5 · طفيف · قابلية القراءةأربع عبارات if متداخلة هي قاعدة واحدة موزّعة على درج: لمعرفة من تصله الرسالة عليك أن تمسك الشروط الأربعة معًا، بينما يطلب PEP 8 الشكل المسطّح.الإصلاح: خروج مبكر — if user is None: return False وهكذا — عندها يبقى الجسم على مستوى إزاحة واحد.
أسماء تخالف دليل الأسلوب
اسم دالة يبدأ بحرف كبير ومتغيرات من حرف واحد: يعترض RuboCop، ويعترض القارئ التالي أيضًا.
راجع هذه الدالة وفق RuboCop: ما الذي يخالف هنا أسلوب Ruby المعتاد؟def CalcTotal(o) t = 0 o.each do |i| t = t + i.price * i.qty end tend
الأسطر 1-4 · طفيف · معاييرNaming/MethodName: أسماء الدوال في Ruby تُكتب snake_case، وCamelCase هنا تُقرأ كأنها ثابت. والأسماء o وt وi لا تقول شيئًا عن محتواها، والجمع اليدوي هو تحديدًا عمل sum.الإصلاح: def calc_total(items) ثم items.sum do |item| item.price * item.qty end — أربعة أسطر تصير سطرًا واحدًا.
نسبة الضريبة في موضعين
قاعدة واحدة منسوخة في دالتين وقد تباعدتا فعلًا: 20 بالمئة في الفاتورة و19 في الإيصال.
راجع هاتين الدالتين: هل تحسبان الشيء نفسه؟def invoice_total(order): return round(order.subtotal * 1.2, 2)def receipt_total(order): return round(order.subtotal * 1.19, 2)
السطران 2 و5 · جسيم · بنيةقاعدة عمل واحدة مكتوبة مرتين وقد تباعدت بالفعل: الفاتورة تحسب 20 بالمئة والإيصال 19، فيرى العميل مبلغين مختلفين لطلب واحد.الإصلاح: ثابت واحد VAT_RATE ودالة واحدة تستخدمها الاثنتان، فتتغير النسبة في موضع واحد فقط.
الاختبارات والموثوقية
ما الذي يحدث حين يتعطّل شيء: كتابة اختبارات تتجاوز المسار السعيد، واستثناء مبتلع يخفي العطل في صمت، ونسخة احتياطية لم يتحقق أحد من إمكانية استعادتها يومًا.
اختبار للمسار السعيد فقط
حالة ناجحة واحدة تمنح إحساسًا بتغطية غير موجودة.
قيّم اختبارات apply_discount هذه: ما الذي ينقصها؟def test_apply_discount(): order = Order(total=100) apply_discount(order, 10) assert order.total == 90
السطر 1 · جسيم · اختباراتالمغطّى حالة واحدة: خصم عادي على طلب غير مدفوع. لا شيء عن الصفر، ولا عن مئة بالمئة، ولا عن قيمة سالبة، ولا عن طلب مدفوع سلفًا، ولا عن التقريب على 33.33 — وأي فرع من هذه قد ينكسر دون أن يلاحظه أحد.الإصلاح: parametrize على القيم الحدية واختبار مستقل بـ pytest.raises للحالة percent=150 وللطلب المدفوع.
استثناء مبتلع
تحوّل except Exception: pass السقوط إلى "ok" وتمحو الأثر من السجلات.
راجع حفظ الملف الشخصي هذا: يقول المستخدمون إن تعديلاتهم تختفي أحيانًا.def save_profile(user, data): try: db.update(user.id, data) search.reindex(user.id) except Exception: pass return "ok"
الأسطر 5-6 · حرج · خطأتبتلع except Exception: pass كل شيء وتعيد "ok" حتى حين لا تتم الكتابة: يرى المستخدم نجاحًا، والبيانات غير موجودة، والسجل فارغ. ووجود عمليتين تحت try واحد يجعل فشل إعادة الفهرسة غير متمايز عن فشل قاعدة البيانات.الإصلاح: التقاط الاستثناءات المحددة، والتسجيل بـ logger.exception، وإعادة حالة صادقة؛ وإخراج إعادة الفهرسة جانبًا كي لا يُلغي فشلها الحفظ.
نسخ احتياطي بلا تحقق
لا ينظر السكربت إلى رموز الخروج ويحذف النسخ القديمة قبل أن يكتشف أحد أن الجديدة فارغة.
راجع سكربت النسخ الاحتياطي الليلي هذا: الاستعادة من آخر نسخة لم تنجح.#!/usr/bin/env bashpg_dump "$DATABASE_URL" > /backup/db.sqlgzip -f /backup/db.sqlfind /backup -name "db.sql.gz" -mtime +7 -delete
الأسطر 2-4 · حرج · خطألا يوجد set -euo pipefail: إذا فشل pg_dump يُنشأ الملف على أي حال فارغًا، ويضغطه gzip بلا اعتراض، ويحذف find النسخ السليمة الأقدم من أسبوع؛ وبعد سبعة أيام لا تبقى نسخة واحدة صالحة. كما يمر $DATABASE_URL غير المضبوط دون أن يلاحظه أحد.الإصلاح: set -euo pipefail في السطر الأول، وفحص حجم النسخة بعد pg_dump، وحذف النسخ القديمة فقط بعد نجاح الجديدة.