05

المحللات

فحص الملفات، ومطابقة أنماط glob، ومحللا JS/TS وPython، وكشف التكرار، وقواعد كشف الأسرار والأسماء

6 دقائق قراءة12 أقسام

المحللات تحوّل نص الكود المصدري إلى نتائج فحص. موجودة في src/analyzers/. هذا الملف يشرح كيف تعمل؛ أما 06 — مرجع القواعد فيشرح ماذا تفحص كل قاعدة.

نظرة عامة

text
                       ┌─▶ jsAnalyzer.analyzeJs()   (.js .jsx .mjs .cjs .ts .tsx)
file text ─ analyzerFor┤
                       └─▶ pyAnalyzer.analyzePy()   (.py)
                                   │
                                   ▼  RawFinding[]
                     duplicateCode.detectDuplicates()
                                   │
                                   ▼
                         model.toViolation()  →  Violation[]

كل محلل له نفس التوقيع:

ts
(relPath: string, text: string, config: CleanCodeConfig) => RawFinding[]

إنها دالة خالصة: لا قراءة ولا كتابة ولا Git. ولهذا يعمل نفس الكود على ملفات القرص (فحص HEAD)، وعلى الـ blobs التاريخية (مرور الـ commits)، وعلى محتوى المحرر غير المحفوظ (التنبيهات المباشرة).

runAnalyzers.ts — فحص HEAD

analyzeProject(root, config, excludeGlobs, onProgress):

  1. scanFiles() تجمع كل ملف بامتداد مدعوم وغير مستثنى.
  2. لكل ملف:
    • يُقرأ بترميز UTF-8 (إذا تعذّرت قراءته يُتخطى)؛
    • إذا لم تكن excludeGenerated مساوية لـ false وكانت ترويسة الملف تقول إنه مولَّد ← يُتخطى ويُحسب في excludedFiles؛
    • يُشغَّل المحلل. خطأ في تحليل الصياغة يتخطى ذلك الملف فقط، وليس التحليل كله.
  3. تُشغَّل detectDuplicates() مرة واحدة على كل الملفات المتبقية.
  4. تُحوَّل كل نتيجة إلى Violation وتُرتَّب حسب الخطورة (critical ← low)، ثم المسار، ثم السطر.

analyzerFor(relPath) تختار المحلل حسب امتداد الملف، أو تعيد null للملفات غير المدعومة.

fileScanner.ts — أي الملفات تُقرأ

  • يستعرض جذر المستودع بشكل متكرر.
  • يتخطى دائماً المجلدات .git وnode_modules و.hg و.svn.
  • يتخطى أي مجلد يطابقه نمط الاستثناء كـ dir أو dir/، فلا يُدخَل المجلد المستثنى أصلاً.
  • يُبقي الملف فقط إذا كان امتداده مدعوماً، وليس مستثنى، وحجمه 1,000,000 بايت أو أقل (MAX_FILE_BYTES). الملفات الأكبر تكون غالباً مولَّدة أو مجمّعة.
  • تُوحَّد المسارات لتستخدم الشرطة / على كل أنظمة التشغيل.

glob.ts — لغة أنماط الاستثناء

تستخدم CleanLens صيغة glob صغيرة خاصة بها، وليست محرّك .gitignore كاملاً.

الصيغةالمعنى
*أي أحرف ما عدا /
**أي أحرف بما فيها / (و**/ تطابق أيضاً صفر مجلدات)
?حرف واحد ما عدا /
أي شيء آخرحرفي (الرموز الخاصة في التعابير النمطية تُهرَّب)

ExcludeMatcher يضيف ميزتين:

  • الأنماط غير المثبتة بالجذر تطابق في أي عمق. النمط venv/** يصبح **/venv/**، فيستثني أيضاً backend/venv/…. الأنماط التي تبدأ بـ / (مثبتة بالجذر) أو بـ **/ تُستخدم كما هي، بعد حذف / في أولها.
  • الأنماط التي ليس فيها / تطابق أيضاً اسم الملف وحده. النمط *.min.js يستثني a/b/c.min.js.

غير مدعوم: النفي (!pattern)، وفئات الأحرف ([abc])، والأقواس المعقوفة ({a,b}).

excludes.ts — قائمة الاستثناءات الكاملة

القائمة النهائية تبنيها mergedExcludeGlobs(). تُضاف المصادر بهذا الترتيب، ولا شيء يحذف إدخالاً سابقاً:

  1. الافتراضيات المدمجة (DEFAULT_EXCLUDE): **/node_modules/** و**/dist/** و**/build/** و**/coverage/** و**/.next/** و**/vendor/** و**/__pycache__/** و**/staticfiles/** و**/generated/** و*.min.js و*.map و*.generated.*.
  2. ملفات الترحيل **/migrations/**، إلا إذا كانت excludeMigrations: false.
  3. ملفات القفل package-lock.json وyarn.lock وpnpm-lock.yaml، إلا إذا كانت excludeLockFiles: false. (هذه لا تُحلَّل أصلاً؛ الخيار يؤثر فقط في قائمة الاستثناءات وبصمة الذاكرة المؤقتة.)
  4. مسارات linguist-generated من .gitattributes، إلا إذا كانت excludeGenerated: false. النمط الذي ليس فيه / يصبح **/<pattern>.
  5. إدخالات .gitignore (بأفضل جهد): تُتخطى التعليقات وحالات النفي !؛ والإدخال الذي ينتهي بـ / أو الاسم الذي ليس فيه نقطة يضيف أيضاً <name>/**.
  6. قائمة exclude في الإعدادات نفسها (قائمة الإعداد المسبق مع إضافاتك، مع إدخالات ملف التخصيص المحلي وإعداد VS Code).

ترويسات الملفات المولَّدة

hasGeneratedHeader(text) تفحص أول 5 أسطر، دون اعتبار لحالة الأحرف، بحثاً عن أي من: @generated وgenerated file وauto-generated وautogenerated وdo not edit وcode generated. الملفات المطابقة تُتخطى في فحص HEAD وفي مرور الـ commits (إلا إذا كانت excludeGenerated: false).

jsAnalyzer.ts — JavaScript وTypeScript

  • التحليل يتم بـ واجهة مترجم TypeScript ‏(ts.createSourceFile)، مع اختيار نوع السكربت من الامتداد (TS وTSX وJSX وJS). لا يوجد فحص أنواع ولا tsconfig؛ الصياغة فقط.
  • يمر على شجرة AST مرة واحدة مع عدّاد depth لقياس التداخل.
  • العُقد "الشبيهة بالدوال": تعريفات الدوال وتعابيرها، والدوال السهمية، والـ methods، والـ constructors، والـ getters والـ setters.
  • التعقيد الدوري (Cyclomatic complexity) يبدأ من 1 ويضيف 1 لكل if و?: وfor وfor…in وfor…of وwhile وdo وcatch وcase غير فارغ، و&& و|| و??. لا يدخل في الدوال المتداخلة؛ هذه تُحسب منفصلة.
  • أسماء الرموز: النتائج المتعلقة بدالة أو class تحمل اسمها (symbolName). هذا يجعل البصمات ثابتة ويتيح ثقة النسب medium.
  • عدّ الاستخدامات لقاعدة detectUnusedCode: يُحسب كل معرّف في الملف مرة واحدة مسبقاً؛ والتعريف الذي يظهر مرة واحدة فقط (هو نفسه) غير مستخدم.

pyAnalyzer.ts — Python

تُحلل Python بدون parser كامل، اعتماداً على الأسطر والمسافة البادئة:

  • تُحوَّل مسافات الـ Tab إلى 4 مسافات؛ وتُحذف تعليقات # (مع مراعاة النصوص بين علامات التنصيص).
  • كتلة def / async def / class تنتهي عند أول سطر غير فارغ مسافته البادئة أقل من أو تساوي مسافة الترويسة.
  • تُقرأ المعاملات من التوقيع (عبر الأسطر حتى ))، مع تجاهل self وcls و*args و**kwargs.
  • التعقيد تقريبي: 1 + الأسطر التي تبدأ بـ if وelif وfor وwhile وexcept وcase + كل and/or في جسم الدالة.
  • عمق التداخل هو indent / 4 على الأسطر التي تبدأ بـ if وelif وelse وfor وwhile وwith وtry وexcept. ويشمل مستويات def/class المحيطة، فيبدأ جسم الـ method من العمق 2.
  • الـ docstring هو أول سطر في جسم الدالة يبدأ بـ """ أو '''.

هذا يجعل محلل Python سريعاً وبلا اعتماديات، لكنه أقل دقة من محلل JS. انظر 17 — القيود.

duplicateCode.ts — الكتل المكررة

  1. تطبيع كل سطر: حذف المسافات الطرفية وضم المسافات. تُحذف الأسطر التي: طولها أقل من 12 حرفاً، أو كلها أقواس/علامات ترقيم، أو تعليقات (// و# و*)، أو أسطر import / from / export.
  2. تمرير نافذة من minLines سطراً محتفظاً به (خيار، افتراضياً 6، وأقل قيمة 3) على كل ملف. تُتخطى النافذة إذا كانت أسطرها موزعة على أكثر من 3 × minLines سطراً أصلياً (غير متصلة بما يكفي).
  3. أول مرة يُرى فيها نص النافذة يُسجَّل. وعندما يُرى مرة أخرى (في نفس الملف أو ملف آخر) يكون الموضع اللاحق تطابقاً.
  4. التطابقات المتداخلة أو المتجاورة تُدمج في نتيجة واحدة لكل كتلة مكررة. نسخة من 40 سطراً هي مخالفة واحدة، وليست 35.

ملاحظات:

  • الظهور الأول لا يُعلَّم أبداً؛ فقط النسخ التي تُكتشف لاحقاً في ترتيب الفحص.
  • في فحص HEAD يعمل الكشف عبر كل الملفات. وفي مرور الـ commits (analyzeRevision) يعمل داخل ملف واحد فقط، لأنه لا يُقارَن إلا ملف واحد قبل وبعد.

secrets.ts — كشف الأسرار المكتوبة في الكود

فحصان مشتركان بين اللغتين:

  • أنماط رموز معروفة في أي مكان داخل النص: مفتاح وصول AWS ‏(AKIA…)، وترويسة مفتاح خاص PEM، ورمز GitHub ‏(ghp_ وgho_ وghu_ وghs_ وghr_)، ورمز Slack ‏(xox?-)، وJWT ‏(eyJ….….…)، ومفاتيح API من نوع sk-….
  • اسم يوحي بسرّ + قيمة تشبه بيانات دخول: اسم المتغير/الخاصية يطابق password وpasswd وsecret وapi_key وaccess_key وauth_token وtoken وcredential وprivate_key وclient_secret، …؛ والقيمة ليست نصاً بديلاً (changeme وyour_… و<…> و${…} وexample وtest وxxx، …)؛ وتبدو كبيانات دخول (بلا مسافات، و8 أحرف آمنة أو أكثر، وفيها أحرف وأرقام أو طولها 20 حرفاً أو أكثر).

names.ts — الأسماء غير الواضحة

  • isUnclearName (المتغيرات المحلية): تعلّم الأسماء من حرفين أو أقل إلا المسموح منها (id وdb وfs وio وui وok وel وfn وcb وx وy وz وi وj وk وn وt، …)؛ والكلمات المبهمة التي يتبعها رقم (data1 وtmp2 وresult3، …)؛ والأحرف القليلة مع أرقام (abc1).
  • isVagueName (المعاملات وأسماء الدوال والـ classes): نفس الشيء بدون فحص الأسماء القصيرة، فيُسمح بـ e وx وcb.
  • مسموح دائماً: الأسماء التي تبدأ بـ _، والأسماء التقنية مثل utf8 وsha256 وbase64 وipv4 وoauth2 وi18n وh1–h6.

model.ts — من نتيجة فحص إلى مخالفة

toViolation(finding, config):

  • severity من config.rules[ruleId].severity (افتراضياً medium)؛
  • category من RULE_CATEGORY؛
  • weight من جدول SEVERITY_WEIGHT الافتراضي (انظر الملاحظة في 08 — التقييم)؛
  • id = قيمة hash من نوع djb2 للقاعدة + المسار + السطر + الرسالة (فريدة داخل التقرير، لكنها غير ثابتة عبر التعديلات؛ محرّك النسب يستخدم البصمة بدلاً منها)؛
  • source = "internal".

دوال مساعدة تستخدمها المحللات:

  • ruleEnabled(config, id) — تعيد true فقط إذا كانت enabled === true. القاعدة غير الموجودة في الإعدادات تكون معطلة.
  • ruleLimit(config, id, fallback) وruleOption(config, id, key, fallback).

analyzeRevision.ts — نسخة تاريخية واحدة من ملف

يستخدمها مرور الـ commits. تشغّل محلل الملف مع كشف التكرار داخل الملف على blob، وتحوّل النتائج إلى مخالفات، وتضيف بصمة لكل منها. وتعيد:

  • [] لأنواع الملفات غير المدعومة؛
  • null عندما يكون النص مفقوداً أو يرمي المحلل خطأ، حتى يحسب المستدعي المخالفات المتأثرة كـ unattributed بدلاً من إسقاطها بصمت.