المحللات
فحص الملفات، ومطابقة أنماط glob، ومحللا JS/TS وPython، وكشف التكرار، وقواعد كشف الأسرار والأسماء
المحللات تحوّل نص الكود المصدري إلى نتائج فحص. موجودة في src/analyzers/. هذا الملف يشرح كيف تعمل؛ أما 06 — مرجع القواعد فيشرح ماذا تفحص كل قاعدة.
نظرة عامة
┌─▶ jsAnalyzer.analyzeJs() (.js .jsx .mjs .cjs .ts .tsx)
file text ─ analyzerFor┤
└─▶ pyAnalyzer.analyzePy() (.py)
│
▼ RawFinding[]
duplicateCode.detectDuplicates()
│
▼
model.toViolation() → Violation[]كل محلل له نفس التوقيع:
(relPath: string, text: string, config: CleanCodeConfig) => RawFinding[]إنها دالة خالصة: لا قراءة ولا كتابة ولا Git. ولهذا يعمل نفس الكود على ملفات القرص (فحص HEAD)، وعلى الـ blobs التاريخية (مرور الـ commits)، وعلى محتوى المحرر غير المحفوظ (التنبيهات المباشرة).
runAnalyzers.ts — فحص HEAD
analyzeProject(root, config, excludeGlobs, onProgress):
scanFiles()تجمع كل ملف بامتداد مدعوم وغير مستثنى.- لكل ملف:
- يُقرأ بترميز UTF-8 (إذا تعذّرت قراءته يُتخطى)؛
- إذا لم تكن
excludeGeneratedمساوية لـfalseوكانت ترويسة الملف تقول إنه مولَّد ← يُتخطى ويُحسب فيexcludedFiles؛ - يُشغَّل المحلل. خطأ في تحليل الصياغة يتخطى ذلك الملف فقط، وليس التحليل كله.
- تُشغَّل
detectDuplicates()مرة واحدة على كل الملفات المتبقية. - تُحوَّل كل نتيجة إلى
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(). تُضاف المصادر بهذا الترتيب، ولا شيء يحذف إدخالاً سابقاً:
- الافتراضيات المدمجة (
DEFAULT_EXCLUDE):**/node_modules/**و**/dist/**و**/build/**و**/coverage/**و**/.next/**و**/vendor/**و**/__pycache__/**و**/staticfiles/**و**/generated/**و*.min.jsو*.mapو*.generated.*. - ملفات الترحيل
**/migrations/**، إلا إذا كانتexcludeMigrations: false. - ملفات القفل
package-lock.jsonوyarn.lockوpnpm-lock.yaml، إلا إذا كانتexcludeLockFiles: false. (هذه لا تُحلَّل أصلاً؛ الخيار يؤثر فقط في قائمة الاستثناءات وبصمة الذاكرة المؤقتة.) - مسارات
linguist-generatedمن.gitattributes، إلا إذا كانتexcludeGenerated: false. النمط الذي ليس فيه/يصبح**/<pattern>. - إدخالات
.gitignore(بأفضل جهد): تُتخطى التعليقات وحالات النفي!؛ والإدخال الذي ينتهي بـ/أو الاسم الذي ليس فيه نقطة يضيف أيضاً<name>/**. - قائمة
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 — الكتل المكررة
- تطبيع كل سطر: حذف المسافات الطرفية وضم المسافات. تُحذف الأسطر التي: طولها أقل من 12 حرفاً، أو كلها أقواس/علامات ترقيم، أو تعليقات (
//و#و*)، أو أسطرimport/from/export. - تمرير نافذة من
minLinesسطراً محتفظاً به (خيار، افتراضياً 6، وأقل قيمة 3) على كل ملف. تُتخطى النافذة إذا كانت أسطرها موزعة على أكثر من3 × minLinesسطراً أصلياً (غير متصلة بما يكفي). - أول مرة يُرى فيها نص النافذة يُسجَّل. وعندما يُرى مرة أخرى (في نفس الملف أو ملف آخر) يكون الموضع اللاحق تطابقاً.
- التطابقات المتداخلة أو المتجاورة تُدمج في نتيجة واحدة لكل كتلة مكررة. نسخة من 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بدلاً من إسقاطها بصمت.