
سوف أخبرك كيف تمكنا من كتابة linter التي تحولت إلى سرعة كافية للتحقق من التغييرات خلال كل دفعة git والقيام بذلك في 5-10 ثواني مع قاعدة الكود من 5 ملايين سطر في PHP. أطلقنا عليه NoVerify.
يدعم NoVerify الأشياء الأساسية مثل الانتقال إلى التعريف والبحث عن الاستخدامات وهو قادر على العمل في وضع
خادم اللغة . بادئ ذي بدء ، تركز الأداة الخاصة بنا على البحث عن الأخطاء المحتملة ، ولكن يمكنها أيضًا التحقق من النمط. اليوم ، ظهرت شفرة المصدر في المصادر المفتوحة على جيثب. ابحث عن الرابط في نهاية المقال.
لماذا نحن بحاجة لدينا linter
في منتصف عام 2018 ، قررنا أن الوقت قد حان لتنفيذ linter لرمز PHP. كان هناك هدفين: تقليل عدد الأخطاء التي يراها المستخدمون ، ومراقبة الالتزام بنمط الشفرة بمزيد من الدقة. كان التركيز الرئيسي على منع الأخطاء النموذجية: وجود متغيرات غير معلنة وغير مستخدمة في الكود ، الكود غير القابل للوصول ، وغيرها. أردت أيضًا أن يعمل المحلل الثابت في أسرع وقت ممكن على قاعدة الكود الخاصة بنا (5-6 مليون سطر من كود PHP في وقت كتابة هذا التقرير).
كما تعلم ربما ، يتم كتابة التعليمات البرمجية المصدر لمعظم الموقع في PHP وتجميعها باستخدام
KPHP ، لذلك سيكون من المنطقي لإضافة هذه الاختبارات إلى المترجم. ولكن في الواقع ، ليس كل التعليمات البرمجية منطقية للتشغيل من خلال KPHP - على سبيل المثال ، المترجم متوافق بشكل ضعيف مع مكتبات الطرف الثالث ، لذلك لا يزال يتم استخدام PHP العادي في بعض أجزاء الموقع. كما أنها مهمة ويجب فحصها من قِبل linter ، لذلك ، للأسف ، لا توجد وسيلة لدمجها في KPHP.
لماذا NoVerify
نظرًا لمقدار كود PHP (سأذكرك أن هذا الرقم يتراوح بين 5 و 6 ملايين سطر) ، لا يمكن "إصلاحه" على الفور حتى يتم اجتياز عمليات الفحص الخاصة بنا في اللغة الداخلية. ومع ذلك ، أريد أن يصبح رمز التغيير أكثر نظافةً وأن يتبع معايير الترميز بشكل أكثر صرامة ، كما أنه يحتوي على أخطاء أقل. لذلك ، قررنا أنه يجب أن يكون linter قادرًا على التحقق من التغييرات التي سيقوم المطور بإطلاقها ، ولا يقسم على الباقي.
للقيام بذلك ، يحتاج linter إلى فهرسة المشروع بالكامل ، وتحليل الملفات بالكامل قبل وبعد التغييرات ، وحساب الفرق بين التحذيرات التي تم إنشاؤها. يتم عرض تحذيرات جديدة للمطور ، ونطالب بإصلاحها قبل إجراء عملية الدفع.
ولكن هناك مواقف يكون فيها هذا السلوك غير مرغوب فيه ، وبعد ذلك يمكن للمطورين الدفع بدون ربط محلي - باستخدام الأمر
git push --no-verify
. الخيار
--no-verify
وأعطى اسمًا إلى linter :)
ماذا كانت البدائل
يستخدم أساس التعليمات البرمجية في VK القليل من OOP ويتكون أساسًا من وظائف وفئات ذات طرق ثابتة. إذا كانت الفئات الموجودة في PHP تدعم التحميل التلقائي ، فإن الوظائف لا تدعمها. لذلك ، لا يمكننا استخدام أجهزة التحليل الثابتة بدون تعديلات كبيرة ، والتي تستند عملها على حقيقة أن التحميل التلقائي سيؤدي إلى تحميل جميع الكود المفقود. مثل هذه الأبراج تشمل ، على سبيل المثال ،
المزمور من فيميو .
درسنا أدوات التحليل الثابتة التالية:
- يتطلب PHPStan - أحادية الخيوط ، التحميل التلقائي ، وقد وصل تحليل قاعدة الكود إلى 30 ٪ في نصف ساعة ؛
- Phan - حتى في الوضع السريع مع 20 عملية ، توقف التحليل بنسبة 5٪ بعد 20 دقيقة ؛
- المزمور - يتطلب التحميل التلقائي ، استغرق التحليل 10 دقائق (ما زلت أريد أن أكون أسرع بكثير) ؛
- PHPCS - يتحقق من النمط ، ولكن ليس المنطق ؛
- phpcf - يتحقق فقط للتنسيق.
كما يمكنك أن تخمن من عنوان المقال ، لا تفي أي من هذه الأدوات بمتطلباتنا ، لذلك كتبنا متطلباتنا.
كيف تم إنشاء النموذج الأولي؟
أولاً ، قررنا إنشاء نموذج أولي صغير من أجل فهم ما إذا كان الأمر يستحق المحاولة لإنشاء lintered كامل. نظرًا لأن أحد المتطلبات المهمة للنترنت هو سرعته ، فبدلاً من PHP اخترنا Go. "سريع" هو تقديم ملاحظات للمطور بأسرع وقت ممكن ، ويفضل ألا يكون ذلك في أكثر من 10-20 ثانية. خلاف ذلك ، تبدأ دورة "تصحيح الكود ، تشغيل اللنتير مرة أخرى" في إبطاء عملية التنمية بشكل كبير وإفساد الحالة المزاجية للناس :)
منذ اختيار Go للنموذج الأولي ، فأنت بحاجة إلى محلل PHP. هناك العديد منهم ، لكن مشروع
محلل php بدا لنا الأكثر نضجا. هذا المحلل اللغوي ليس مثاليًا ولا يزال قيد التطوير ، ولكنه مناسب تمامًا لأغراضنا.
بالنسبة للنموذج الأولي ، تقرر محاولة تنفيذ أحد عمليات التفتيش الأكثر بساطة ، للوهلة الأولى: الوصول إلى متغير غير محدد.
تبدو الفكرة الأساسية لتطبيق مثل هذا الفحص بسيطة: لكل فرع (على سبيل المثال ، إذا) ، قم بإنشاء نطاق متداخل منفصل والجمع بين أنواع المتغيرات عند الخروج منه. مثال:
<?php if (rand()) { $a = 42;
يبدو بسيطا ، أليس كذلك؟ في حالة البيانات الشرطية العادية ، كل شيء يعمل بشكل جيد. ولكن يجب علينا التعامل ، على سبيل المثال ، التبديل دون انقطاع.
<?php switch (rand()) { case 1: $a = 1;
لا يتضح على الفور من الكود أن $ c سيتم تعريفه دائمًا في الواقع. على وجه التحديد ، هذا المثال خيالي ، لكنه يوضح جيدًا ما هي اللحظات الصعبة التي يواجهها اللتر (وللشخص في هذه الحالة أيضًا).
النظر في مثال أكثر تعقيدا:
<?php exec("hostname", $out, $retval); echo $out, $retval;
دون معرفة توقيع وظيفة exec ، لا يمكن القول ما إذا كان سيتم تحديد $ out و $ retval. يمكن الحصول على توقيعات الوظائف المدمجة من مستودع
github.com/JetBrains/phpstorm-stubs . ولكن نفس المشاكل ستحدث عند استدعاء الوظائف المعرفة من قبل المستخدم ، ويمكن العثور على توقيعهم فقط عن طريق فهرسة المشروع بأكمله. تأخذ الدالة exec الوسيطتين الثانية والثالثة حسب المرجع ، مما يعني أنه يمكن تحديد المتغيرات $ out و $ retval. هنا ، لا يعد الوصول إلى هذه المتغيرات خطأ بالضرورة ، ويجب ألا يقسم linter بهذا الرمز.
تنشأ مشكلات مماثلة مع تمرير الارتباط الضمني مع الطرق ، ولكن في نفس الوقت ، تتم إضافة الحاجة إلى استنتاج أنواع المتغيرات:
<?php if (rand()) { $a = some_func(); } else { $a = other_func(); } $a->some_method($b); echo $b;
نحتاج إلى معرفة أنواع دالات some_func () و other_func () لإرجاعها فيما بعد للعثور على طريقة تسمى some_method في هذه الفئات. عندها فقط يمكننا أن نقول ما إذا كان سيتم تحديد المتغير $ b أم لا. الموقف معقد بسبب حقيقة أن الوظائف والأساليب البسيطة في كثير من الأحيان لا تحتوي على تعليقات phpdoc ، لذلك لا تزال بحاجة إلى أن تكون قادرًا على حساب أنواع الوظائف والأساليب بناءً على تنفيذها.
عند تطوير النموذج الأولي ، اضطررت إلى تنفيذ ما يقرب من نصف جميع الوظائف بحيث تعمل أبسط عمليات التفتيش كما ينبغي.
العمل كخادم لغة
لتسهيل تصحيح منطق linter وأسهل رؤية التحذيرات التي يصدرها ، قررنا إضافة وضع التشغيل كخادم
لغة لـ PHP . في وضع التكامل مع Visual Studio Code ، يبدو الأمر كما يلي:

في هذا الوضع ، من المريح اختبار الفرضيات واختبار الحالات المعقدة (بعد ذلك تحتاج إلى كتابة الاختبارات ، بالطبع). من الجيد أيضًا اختبار الأداء: حتى في الملفات الكبيرة ، يُظهر php-parser on Go سرعة جيدة.
يعد دعم خادم اللغة بعيدًا عن المثالية ، لأن الغرض الرئيسي منه هو تصحيح قواعد اللنت. ومع ذلك ، في هذا الوضع هناك العديد من الميزات الإضافية:
- نصائح للأسماء المتغيرة والثوابت والوظائف والخصائص والأساليب.
- تمييز الأنواع المشتقة من المتغيرات.
- انتقل إلى التعريف.
- البحث عن الاستخدامات.
"كسول" نوع الاستدلال
في وضع خادم اللغة ، يلزم العمل التالي: يمكنك تغيير الرمز في ملف واحد ، وبعد ذلك ، عند التبديل إلى آخر ، يجب عليك التعامل مع المعلومات المحدثة بالفعل حول الأنواع التي يتم إرجاعها في الوظائف أو الطرق. تخيل الملفات التي يتم تحريرها بالترتيب التالي:
<?php
نظرًا لأننا لا نجبر المطورين على كتابة PHPDoc دائمًا (خصوصًا في مثل هذه الحالات البسيطة) ، فنحن بحاجة إلى طريقة لتخزين المعلومات حول نوع دالة B :: something () التي يتم إرجاعها. لذلك عندما يتغير ملف A.php ، يتم تحديث معلومات النوع في ملف C.php على الفور.
أحد الحلول الممكنة هو تخزين "الأنواع البطيئة". على سبيل المثال ، نوع الإرجاع للأسلوب B :: something () هو في الواقع نوع تعبير (جديد A) -> prop. في هذا النموذج ، يقوم linter بتخزين معلومات حول النوع ، وبفضل هذا ، يمكنك تخزين جميع المعلومات التعريفية لكل ملف مؤقت وتحديثها فقط عندما يتغير هذا الملف. يجب أن يتم ذلك بعناية حتى لا تتسرب معلومات محددة للغاية عن الأنواع. من الضروري أيضًا تغيير إصدار ذاكرة التخزين المؤقت عندما يتغير منطق استنتاج الكتابة. ومع ذلك ، فإن هذا التخزين المؤقت يسرع مرحلة الفهرسة (التي سأناقشها لاحقًا) بمقدار 5 إلى 10 مرات مقارنةً بالتحليل المتكرر لجميع الملفات.
مرحلتان من العمل: الفهرسة والتحليل
كما نتذكر ، حتى بالنسبة لأبسط تحليل للشفرات ، فإن المعلومات مطلوبة على الأقل حول جميع الوظائف والأساليب في المشروع. هذا يعني أنه لا يمكنك تحليل ملف واحد فقط بشكل منفصل عن المشروع. ومع ذلك - لا يمكن القيام بذلك في مسار واحد: على سبيل المثال ، يسمح لك PHP بالوصول إلى الوظائف التي تم الإعلان عنها أكثر في الملف.
بسبب هذه القيود ، يتألف تشغيل linter من مرحلتين: الفهرسة الأولية والتحليل اللاحق للملفات الضرورية فقط. الآن المزيد عن هاتين المرحلتين.
مرحلة الفهرسة
في هذه المرحلة ، يتم تحليل جميع الملفات وإجراء تحليل محلي لرمز الأساليب والوظائف ، وكذلك الرمز في المستوى الأعلى (على سبيل المثال ، لتحديد أنواع المتغيرات العامة). يتم جمع معلومات حول المتغيرات العامة المعلنة والثوابت والوظائف والفئات وأساليبها وكتابتها إلى ذاكرة التخزين المؤقت. لكل ملف في المشروع ، ذاكرة التخزين المؤقت ملف منفصل على القرص.
يتم تجميع قاموس عالمي لجميع المعلومات الوصفية حول المشروع ، والذي لا يتغير في المستقبل ، * من قطع فردية.
* بالإضافة إلى وضع التشغيل كخادم لغة ، عند إجراء فهرسة وتحليل الملف الذي تم تغييره لكل تعديل.مرحلة التحليل
في هذه المرحلة ، يمكننا استخدام المعلومات الوصفية (حول الوظائف والفئات ...) وتحليل الشفرة مباشرة بالفعل. فيما يلي قائمة بما يمكن التحقق من NoVerify افتراضيًا:
- كود غير قابل للوصول
- الوصول إلى الكائنات كصفيف ؛
- عدم كفاية عدد الوسائط عند استدعاء الوظيفة ؛
- استدعاء طريقة غير معروفة / وظيفة ؛
- الوصول إلى خاصية الفئة المفقودة / الثابت ؛
- قلة الصف
- PHPDoc غير صالح
- الوصول إلى متغير غير محدد ؛
- الوصول إلى متغير لم يتم تعريفه دائمًا ؛
- عدم وجود "كسر" بعد حالة في التبديل / حالة يبني.
- خطأ في بناء الجملة
- متغير غير مستخدم.
القائمة قصيرة جدًا ، ولكن يمكنك إضافة شيكات خاصة بمشروعك.
أثناء تشغيل linter ، اتضح أن الفحص الأكثر فائدة هو الأخير (المتغير غير المستخدم). يحدث هذا غالبًا عند إعادة تشكيل الكود (أو كتابة رمز جديد) وختمه في اسم المتغير: هذا الرمز صالح من وجهة نظر PHP ، ولكنه خاطئ في المنطق.
سرعة العمل
ما المدة التي يستغرقها التغيير الذي نريد دفعه؟ كل هذا يتوقف على عدد الملفات. باستخدام NoVerify ، يمكن أن تستغرق العملية ما يصل إلى دقيقة واحدة (كانت عندما قمت بتغيير 1400 ملف في المستودع) ، ولكن إذا كانت هناك بعض التعديلات ، فعادةً ما تمر كل عمليات الفحص في غضون 4-5 ثواني. خلال هذا الوقت ، يتم فهرسة المشروع بالكامل ، وتحليل الملفات الجديدة ، وكذلك تحليلها. تمكنا تمامًا من إنشاء linter لـ PHP ، والذي يعمل بسرعة حتى مع قاعدة الكود الكبيرة الخاصة بنا.
ما هي النتيجة؟
نظرًا لأن الحل مكتوب في Go ، فمن الضروري استخدام مستودع
github.com/JetBrains/phpstorm-stubs من أجل الحصول على تعريفات لجميع الوظائف والفئات المدمجة في PHP. في المقابل ، حصلنا على سرعة عالية في العمل (فهرسة مليون خط في الثانية ، تحليل 100 ألف سطر في الثانية الواحدة) وتمكنا من إضافة الشيكات باستخدام linter كإحدى الخطوات الأولى في ربط خطاف الدفع.
تم تطوير قاعدة ملائمة لإنشاء عمليات تفتيش جديدة وتحقيق مستوى من فهم التعليمات البرمجية بالقرب من PHPStorm. نظرًا لحقيقة أنه من خارج الصندوق ، يتم دعم الوضع مع حساب الاختلاف ، فمن الممكن تحسين الكود تدريجيًا ، مع تجنب الإنشاءات الجديدة التي قد تكون مشكلة في الكود الجديد.
حساب الفرق ليس مثاليًا: على سبيل المثال ، إذا تم تقسيم ملف واحد كبير إلى عدة ملفات صغيرة ، فلن تتمكن git ، وبالتالي NoVerify ، من تحديد أن الرمز قد تم نقله ، وسوف يتطلب linter إصلاح جميع المشكلات الموجودة. في هذا الصدد ، فإن حساب الفرق يمنع إعادة البناء على نطاق واسع ، لذلك في مثل هذه الحالات يتم تعطيله في كثير من الأحيان.
تتمتع ميزة كتابة linter على Go بميزة أخرى: ليس فقط محلل AST أسرع ويستهلك ذاكرة أقل من PHP ، لكن التحليل اللاحق سريع للغاية أيضًا مقارنة بأي شيء يمكن إجراؤه في PHP. هذا يعني أن linter لدينا يمكنه إجراء تحليل أكثر تعقيدًا وأعمق للرمز ، مع الحفاظ على الأداء العالي (على سبيل المثال ، تتطلب ميزة "الأنواع البطيئة" عددًا كبيرًا من العمليات الحسابية في العملية).
المصدر المفتوح
NoVerify المتاحة على المصدر المفتوح على جيثباستمتع باستخدامك في مشروعك!
محدث: لقد قمت بإعداد
عرض توضيحي يعمل من خلال WebAssembly . القيد الوحيد لهذا العرض التوضيحي هو عدم وجود تعريفات للوظيفة من stubs-stubs ، وبالتالي فإن القسم سوف يقسم على وظائف مدمجة.
يوري Nasretdinov ، مطور قسم البنية التحتية في فكونتاكتي