تحليل ثابت لشفرة PHP باستخدام PHPStan و Phan و Psalm كمثال



إن Badoo موجودة منذ أكثر من 12 عامًا. لدينا الكثير من كود PHP (ملايين الأسطر) وربما تم الاحتفاظ حتى بالخطوط المكتوبة قبل 12 عامًا. لدينا كود مكتوب في أيام PHP 4 و PHP 5. نقوم بنشر الرمز مرتين في اليوم ، ويحتوي كل تخطيط على حوالي 10-20 مهمة. بالإضافة إلى ذلك ، يمكن للمبرمجين نشر تصحيحات عاجلة - تغييرات صغيرة. وفي يوم هذه البقع ، نحصل على عشرات. بشكل عام ، يتغير رمزنا بنشاط كبير.

نحن نبحث باستمرار عن فرص لتسريع التطوير وتحسين جودة الشفرة. لذا قررنا ذات يوم تنفيذ تحليل الكود الثابت. ما جاء منها ، قرأ تحت القطع.

أنواع صارمة: لماذا لا نستخدمها بعد


ذات مرة ، بدأت مناقشة في محادثة PHP الخاصة بشركتنا. وأخبر أحد الموظفين الجدد كيف قدموا في مكان العمل السابق تلميحات إلزامية صارمة من الأنواع العددية لكود كامل - وهذا يقلل بشكل كبير من عدد الأخطاء في الإنتاج.

معظم الدردشة القديمة كانت ضد مثل هذا الابتكار. كان السبب الرئيسي هو أن PHP ليس لديه مترجم يقوم بالتحقق من جميع الأنواع في التعليمات البرمجية في وقت الترجمة ، وإذا لم يكن لديك تغطية 100 ٪ من التعليمات البرمجية مع الاختبارات ، فهناك دائمًا خطر أن الأخطاء سوف تنبثق عند الإنتاج ، والتي تريد السماح.

وبالطبع ، ستجد الأنواع الصارمة نسبة معينة من الأخطاء التي تسببها عدم تطابق النوع وكيف يقوم PHP "بصمت" بتحويل الأنواع. لكن العديد من مبرمجي PHP ذوي الخبرة يعرفون بالفعل كيف يعمل نظام النوع في PHP ، وبواسطة ما يحدث تحويل نوع القواعد ، وفي معظم الحالات يكتبون كود العمل الصحيح.

لكن فكرة وجود نظام معين يظهر حيث يوجد عدم تطابق في الكود البرمجي ، أحببنا. فكرنا في بدائل للأنواع الصارمة.

في البداية أردنا تصحيح PHP. أردنا أنه إذا قبلت الدالة نوعًا ما من نوع عددي (قل int) ، ونوع عددي آخر (مثل تعويم) ، فلن يتم طرح TypeError (وهو في الأساس استثناء) ، ولكن سيحدث تحويل النوع ، وكذلك تسجيل هذا الحدث في error.log. سيتيح لنا هذا العثور على جميع الأماكن التي تكون فيها افتراضاتنا حول الأنواع غير صحيحة. لكن هذا التصحيح بدا محفوفًا بالمخاطر بالنسبة لنا ، وحتى يمكن أن تكون هناك مشاكل مع التبعيات الخارجية التي لم تكن جاهزة لمثل هذا السلوك.

لقد تخلينا عن فكرة تصحيح PHP ، ولكن مع مرور الوقت تزامن كل ذلك مع الإصدارات الأولى من محلل Phan الثابت ، الالتزامات الأولى التي قام بها Rasmus Lerdorf نفسه. لذلك توصلنا إلى فكرة تجربة محللات الشفرة الثابتة.

ما هو تحليل الرمز الثابت؟


يقوم محللو الكود الثابت بقراءة الكود ومحاولة العثور على الأخطاء فيه. يمكنهم إجراء فحوصات بسيطة وواضحة للغاية (على سبيل المثال ، لوجود فئات وأساليب ووظائف وأكثر صعوبة (على سبيل المثال ، ابحث عن عدم تطابق النوع أو ظروف العرق أو الثغرات في التعليمات البرمجية). المفتاح هو أن المحللون لا ينفذون التعليمات البرمجية - فهم تحليل نص البرنامج والتحقق من وجود أخطاء نموذجية (وليس كذلك).

المثال الأكثر وضوحًا لمحلل رمز PHP الثابت هو عمليات الفحص في PHPStorm: عندما تكتب رمزًا ، فإنه يسلط الضوء على المكالمات غير الصحيحة للوظائف والأساليب وعدم تطابق نوع المعلمة وما إلى ذلك. ومع ذلك ، لا يعمل PHPStorm على تشغيل كود PHP الخاص بك - بل يقوم فقط بتحليله.

ألاحظ أنه في هذه المقالة نتحدث عن المحللين الذين يبحثون عن أخطاء في التعليمات البرمجية. هناك فئة أخرى من المحللين - يقومون بفحص نمط كتابة الشفرة ، التعقيد السيكلومي ، أحجام الطريقة ، أطوال الخطوط ، إلخ. نحن لا نعتبر مثل هذه المحللات هنا.

على الرغم من أنه ليس كل ما يعثر عليه المحللون الذين نعتبرهم خطأ بالضبط. عن طريق الخطأ ، أعني الرمز الذي سينشئه Fatal عند الإنتاج. في كثير من الأحيان ، ما يجده المحللون هو على الأرجح عدم الدقة. على سبيل المثال ، قد يتم تحديد نوع معلمة غير صحيح في PHPDoc. لا تؤثر عدم الدقة هذه على تشغيل الكود ، ولكن في وقت لاحق سيتطور الكود - قد يرتكب مبرمج آخر خطأ.

محلل كود PHP الحالي


هناك ثلاثة أجهزة تحليل كود PHP شائعة:

  1. PHPStan .
  2. مزمور .
  3. فان .

وهناك Exakat ، التي لم نجربها .

من جانب المستخدم ، فإن جميع المحللين الثلاثة متماثلون: تقوم بتثبيتها (على الأرجح من خلال Composer) ، وتهيئتها ، وبعد ذلك يمكنك بدء تحليل المشروع بأكمله أو مجموعة الملفات. كقاعدة ، يمكن للمحلل عرض النتائج بشكل جميل في وحدة التحكم. يمكنك أيضًا إخراج النتائج بتنسيق JSON واستخدامها في CI.

جميع المشاريع الثلاثة تتطور بنشاط الآن. المشرفون عليهم نشطون جدًا في الرد على المشكلات في GitHub. غالبًا في اليوم الأول بعد إنشاء تذكرة ، يتفاعلون معها على الأقل (تعليق أو وضع علامة مثل خطأ / تحسين). تم إصلاح العديد من الأخطاء التي وجدناها في غضون يومين. لكني أحب بشكل خاص حقيقة أن مشرفي المشروع يتواصلون بنشاط مع بعضهم البعض ، ويبلغون بعضهم البعض ويرسلون طلبات السحب.

قمنا بتطبيق واستخدام جميع أجهزة التحليل الثلاثة. لكل منها الفروق الدقيقة الخاصة بها ، والبق الخاصة بها. لكن استخدام ثلاثة محللين في نفس الوقت يسهل فهم مكان المشكلة الحقيقية وأين الإيجابية الكاذبة.

ما يمكن أن يفعله المحللون


تحتوي المحللات على العديد من الميزات الشائعة ، لذلك سنلقي نظرة أولاً على ما يمكنها فعله جميعًا ، ثم ننتقل إلى ميزات كل منها.

الشيكات القياسية


بالطبع ، يقوم المحللون بجميع فحوصات الكود القياسية لحقيقة:

  • لا يحتوي الرمز على أخطاء في بناء الجملة ؛
  • جميع الفئات والأساليب والوظائف والثوابت موجودة ؛
  • المتغيرات موجودة ؛
  • في PHPDoc ، التلميحات صحيحة.

بالإضافة إلى ذلك ، يقوم المحللون بالتحقق من الشفرة بحثًا عن الحجج والمتغيرات غير المستخدمة. تؤدي العديد من هذه الأخطاء إلى حالات وفاة حقيقية في الشفرة.

للوهلة الأولى ، قد يبدو أن المبرمجين الجيدين لا يرتكبون مثل هذه الأخطاء ، لكننا في بعض الأحيان في عجلة من أمرنا ، وأحيانًا نلصق ، وأحيانًا نكون غير مهتمين. وفي مثل هذه الحالات ، توفر هذه الفحوصات الكثير.

تدقيق نوع البيانات


بالطبع ، يقوم المحللون الثابتون أيضًا بفحوصات قياسية فيما يتعلق بأنواع البيانات. إذا كان مكتوبًا في الرمز الذي تقبل الوظيفة ، على سبيل المثال ، int ، فسيقوم المحلل بالتحقق مما إذا كانت هناك أماكن يتم فيها تمرير كائن إلى هذه الوظيفة. بالنسبة لمعظم المحللين ، يمكنك تكوين شدة الاختبار ومحاكاة الأنواع الصارمة: تحقق من عدم تمرير أي سلاسل أو منطقية لهذه الوظيفة.

بالإضافة إلى عمليات الفحص القياسية ، لا يزال أمام المحللين الكثير للقيام به.

أنواع الاتحاد

يدعم جميع المحللين مفهوم أنواع الاتحاد. افترض أن لديك وظيفة مثل:

/** * @var string|int|bool $yes_or_no */ function isYes($yes_or_no) :bool {    if (\is_bool($yes_or_no)) {         return $yes_or_no;     } elseif (is_numeric($yes_or_no)) {         return $yes_or_no > 0;     } else {         return strtoupper($yes_or_no) == 'YES';     } } 

محتوياته ليست مهمة للغاية - نوع string|int|bool معلمة الإدخال string|int|bool مهم. أي أن المتغير $yes_or_no هو إما سلسلة أو عدد صحيح أو Boolean .

باستخدام PHP ، لا يمكن وصف هذا النوع من معلمات الوظائف. ولكن في PHPDoc ، هذا ممكن ، ويفهمه العديد من المحررين (مثل PHPStorm).

في أجهزة التحليل الثابتة ، يُسمى هذا النوع نوع الاتحاد ، وهم جيدون جدًا في التحقق من أنواع البيانات هذه. على سبيل المثال ، إذا كتبنا الوظيفة أعلاه مثل هذه (بدون التحقق من قيمة Boolean ):

 /** * @var string|int|bool $yes_or_no */ function isYes($yes_or_no) :bool {    if (is_numeric($yes_or_no)) {        return $yes_or_no > 0;    } else {        return strtoupper($yes_or_no) == 'YES';    } } 

سيرى المحللون أن أي سلسلة أو منطقية يمكن أن تأتي إلى strtoupper ، وتعيد خطأ - لا يمكنك تمرير منطقية إلى strtoupper.

يساعد هذا النوع من الفحص المبرمجين على معالجة الأخطاء أو المواقف بشكل صحيح عندما يتعذر على الدالة إرجاع البيانات. غالبًا ما نكتب دوال يمكنها إرجاع بعض البيانات أو null :

 // load()  null   \User $User = UserLoader::load($user_id); $User->getName(); 

في حالة مثل هذا الرمز ، سيخبرك المحلل أن المتغير $User هنا يمكن أن يكون null ويمكن أن يؤدي هذا الرمز إلى فادح.

اكتب خطأ

في لغة PHP نفسها ، هناك عدد غير قليل من الوظائف التي يمكن أن ترجع إما بعض القيمة أو خطأ. إذا كنا سنكتب مثل هذه الوظيفة ، فكيف نوثق نوعها؟

          /** @return resource|bool */ function fopen(...) {       … } 

بشكل رسمي ، يبدو أن كل شيء صحيح هنا: إرجاع fopen إما مورد أو false (وهو من نوع Boolean ). ولكن عندما نقول أن دالة ترجع نوعًا ما من نوع البيانات ، فهذا يعني أنه يمكنها إرجاع أي قيمة من مجموعة تنتمي إلى نوع البيانات هذا. في مثالنا ، بالنسبة للمحلل ، هذا يعني أن fopen() يمكنه إرجاع true . وعلى سبيل المثال ، في حالة مثل هذا الرمز:

 $fp = fopen('some.file','r'); if($fp === false) {     return false; } fwrite($fp, "some string"); 

يشكو المحللون من أن fwrite يقبل مورد المعلمة الأول ، bool (لأن المحلل يرى أن الخيار الحقيقي ممكن). لهذا السبب ، يفهم جميع المحللين هذا النوع من البيانات "الاصطناعية" على أنه false ، وفي مثالنا يمكننا كتابة @return false|resource يفهم PHPStorm أيضًا وصف هذا النوع.

أشكال الصفيف

في كثير من الأحيان ، يتم استخدام المصفوفات في PHP كنوع record - بنية بها قائمة واضحة من الحقول ، حيث يكون لكل حقل نوعه الخاص. بالطبع ، يستخدم العديد من المبرمجين بالفعل فصولًا لهذا الغرض. ولكن لدينا الكثير من الرموز القديمة في Badoo ، وتستخدم المصفوفات بنشاط هناك. ويحدث أيضًا أن المبرمجين كسالى جدًا لإنشاء فئة منفصلة لبعض التركيبات لمرة واحدة ، وفي مثل هذه الأماكن ، يتم استخدام الصفائف أيضًا.

المشكلة في مثل هذه المصفوفات هي أنه لا يوجد وصف واضح لهذه البنية (قائمة الحقول وأنواعها) في التعليمات البرمجية. يمكن للمبرمجين ارتكاب أخطاء عند العمل مع مثل هذا الهيكل: نسيان الحقول المطلوبة أو إضافة المفاتيح "اليسرى" ، مما يزيد من إرباك التعليمات البرمجية.

تسمح لك المحللون بإدخال وصف لهذه الهياكل:

 /** @param array{scheme:string,host:string,path:string} $parsed_url */ function showUrl(array $parsed_url) { … } 

في هذا المثال ، وصفنا مصفوفة بثلاثة حقول سلسلة: scheme, host path . إذا انتقلنا داخل الوظيفة إلى حقل آخر ، فسوف يظهر المحلل خطأ.

إذا لم تصف الأنواع ، فسيحاول المحللون "تخمين" بنية المصفوفة ، ولكن ، كما تظهر الممارسة ، فإنهم لا ينجحون حقًا في التعليمات البرمجية الخاصة بنا. :)

هذا النهج له عيب واحد. افترض أن لديك بنية يتم استخدامها بنشاط في التعليمات البرمجية. لا يمكنك التصريح عن نوع كاذب في مكان واحد ثم استخدامه في كل مكان. سيكون عليك تسجيل PHPDoc مع وصف الصفيف في كل مكان في الكود ، وهو أمر غير مريح للغاية ، خاصة إذا كان الصفيف يحتوي على العديد من الحقول. سيكون من الصعب أيضًا تعديل هذا النوع لاحقًا (إضافة الحقول وإزالتها).

وصف أنواع مفاتيح الصفيف

في PHP ، يمكن أن تكون مفاتيح الصفيف أعدادًا صحيحة وسلاسل. يمكن أن تكون الأنواع مهمة في بعض الأحيان للتحليل الساكن (وكذلك للمبرمجين). تسمح لك المحللات الثابتة بوصف مفاتيح المصفوفة في PHPDoc:

 /** @var array<int, \User> $users */ $users = UserLoaders::loadUsers($user_ids); 

في هذا المثال ، باستخدام PHPDoc ، أضفنا تلميحًا بأن المفاتيح في مجموعة $users هي عدد صحيح ، والقيم هي كائنات من فئة \User . يمكننا وصف النوع باسم \ User []. سيخبر هذا المحلل أن هناك كائنات في فئة \User في الصفيف ، ولكن لن يخبرنا أي شيء عن نوع المفاتيح.

يدعم PHPStorm هذا التنسيق لوصف المصفوفات بدءًا من الإصدار 2018.3.

مساحة الاسم الخاصة بك في PHPDoc

يمكن لـ PHPStorm (والمحررين الآخرين) والمحللين الثابتين فهم PHPDoc بشكل مختلف. على سبيل المثال ، يدعم المحللون هذا التنسيق:

 /** @param array{scheme:string,host:string,path:string} $parsed_url */ function showUrl($parsed_url) { … } 

لكن PHPStorm لا يفهمه. لكن يمكننا أن نكتب هكذا:

 /** * @param array $parsed_url * @phan-param array{scheme:string,host:string,path:string} $parsed_url * @psalm-param array{scheme:string,host:string,path:string} $parsed_url */ function showUrl($parsed_url) { … } 

في هذه الحالة ، سيتم إرضاء كل من المحللين و PHPStorm. ستستخدم PHPStormparam ، وسيستخدم المحللون علامات PHPDoc الخاصة بهم.

الشيكات ميزات PHP


يتم توضيح هذا النوع من الاختبار بشكل أفضل من خلال المثال.

هل نعلم جميعًا ما يمكن أن ترجعه الدالة explode () ؟ إذا ألقيت نظرة على الوثائق ، يبدو أنها ترجع مصفوفة. ولكن إذا قمت بفحصها بعناية أكبر ، فسوف نرى أنها يمكن أن ترجع خطأ أيضًا. في الواقع ، يمكن أن يُرجع كلا من الخطأ والخطأ إذا قمت بتمريره إلى الأنواع الخاطئة ، ولكن إرسال القيمة الخاطئة بنوع البيانات الخاطئ هو بالفعل خطأ ، لذا فإن هذا الخيار غير مثير للاهتمام بالنسبة لنا الآن.

بشكل رسمي ، من وجهة نظر المحلل ، إذا كان بإمكان دالة إرجاع false أو مصفوفة ، فمن المرجح أن تحقق الشفرة من false. لكن دالة explode () تُرجع خطأ فقط إذا كان المحدد (المعلمة الأولى) يساوي سلسلة فارغة. غالبًا ما تكون مكتوبة بشكل صريح في الشفرة ، ويستطيع المحللون التحقق من أنها ليست فارغة ، مما يعني أنه في هذا المكان تقوم الدالة explode () بإرجاع مصفوفة بدقة ولا حاجة لفحص خاطئ.

تحتوي PHP على بعض الميزات. يضيف المحللون تدريجيًا الشيكات المناسبة أو يحسنونها ، ولم نعد نحن المبرمجين بحاجة إلى تذكر كل هذه الميزات.

ننتقل إلى وصف المحللين محددة.

PHPStan


تطوير بعض Ondřej Mirtes من جمهورية التشيك. تم تطويره بنشاط منذ نهاية عام 2016.

لبدء استخدام PHPStan ، تحتاج إلى:

  1. قم بتثبيته (أسهل طريقة للقيام بذلك هي من خلال Composer).
  2. (اختياري) تكوين.
  3. في أبسط الحالات ، قم بتشغيل:

vendor/bin/phpstan analyse ./src

(بدلاً من src قد تكون هناك قائمة بالملفات المحددة التي تريد التحقق منها).

سيقرأ PHPStan كود PHP من الملفات المنقولة. إذا واجه فئات غير معروفة ، فسيحاول تحميلها بالتحميل التلقائي ومن خلال التفكير لفهم واجهتها. يمكنك أيضًا نقل المسار إلى ملف Bootstrap ، الذي تقوم من خلاله بتكوين التحميل التلقائي ، وربط بعض الملفات الإضافية لتبسيط تحليل PHPStan.

الميزات الرئيسية:

  1. من الممكن تحليل ليس قاعدة التعليمات البرمجية بالكامل ، ولكن فقط جزء - فئات غير معروفة PHPStan سيحاول تحميل التحميل التلقائي.
  2. إذا لم تكن بعض صفوفك في التحميل التلقائي لسبب ما ، فلن تتمكن PHPStan من العثور عليها وستظهر خطأ.
  3. إذا كنت تستخدم بنشاط الأساليب السحرية من خلال __call / __get / __set ، فيمكنك كتابة مكون إضافي لـ PHPStan. توجد بالفعل مكونات إضافية لـ Symfony و Doctrine و Laravel و Mockery وما إلى ذلك.
  4. في الواقع ، يقوم PHPStan بالتحميل التلقائي ليس فقط للفئات غير المعروفة ، ولكن بشكل عام للجميع. لدينا الكثير من التعليمات البرمجية القديمة المكتوبة قبل ظهور فصول مجهولة ، عندما نقوم بإنشاء فئة في ملف واحد ، ثم نقوم على الفور بنسخها ، وربما حتى استدعاء بعض الطرق. يؤدي التحميل التلقائي ( include ) لهذه الملفات إلى أخطاء ، لأن الشفرة لا يتم تنفيذها في بيئة عادية.
  5. Configs بتنسيق النيون (لم أسمع أبدًا أن مثل هذا التنسيق تم استخدامه في مكان آخر).
  6. لا يوجد دعم لعلامات @phpstan-var, @phpstan-return الخاصة بهم مثل @phpstan-var, @phpstan-return ، إلخ.

ميزة أخرى هي أن الأخطاء لها نص ، ولكن لا يوجد نوع. أي ، يتم إرجاع نص الخطأ إليك ، على سبيل المثال:

  • Method \SomeClass::getAge() should return int but returns int|null
  • Method \SomeOtherClass::getName() should return string but returns string|null

في هذا المثال ، يتعلق كلا الخطأين بشكل أساسي بنفس الشيء: يجب أن تعرض الطريقة نوعًا واحدًا ، ولكنها في الواقع ترجع النوع الآخر. لكن نصوص الأخطاء مختلفة ، على الرغم من أنها متشابهة. لذلك ، إذا كنت تريد تصفية أي أخطاء في PHPStan ، فقم بذلك فقط من خلال التعبيرات العادية.

للمقارنة ، في أنواع أخرى من المحللين ، يكون للأخطاء نوع. على سبيل المثال ، في Phan ، يكون مثل هذا الخطأ من النوع PhanPossiblyNullTypeReturn ، ويمكنك تحديد في التكوين أنك لست بحاجة إلى التحقق من وجود مثل هذه الأخطاء. أيضًا ، بعد وجود نوع الخطأ ، من الممكن ، على سبيل المثال ، جمع إحصاءات الأخطاء بسهولة.

نظرًا لأننا لا نستخدم Laravel و Symfony و Doctrine والحلول المماثلة ، ونادراً ما نستخدم أساليب سحرية في التعليمات البرمجية الخاصة بنا ، فقد اتضح لنا أن الميزة الرئيسية لـ PHPStan لم تتم المطالبة بها. ؛ (علاوة على ذلك ، نظرًا لحقيقة أن PHPStan يشمل - يتم فحص جميع الفئات ، في بعض الأحيان لا يعمل تحليله ببساطة على أساس الكود الخاص بنا.

ومع ذلك ، لا يزال PHPStan مفيدًا لنا:

  • إذا كنت بحاجة إلى التحقق من عدة ملفات ، فإن PHPStan أسرع بشكل ملحوظ من Phan وأسرع قليلاً (20-50 ٪) من Psalm.
  • تقارير PHPStan تسهل العثور false-positive في محللين آخرين. عادة ، إذا كان هناك بعض fatal الصريحة في الشفرة ، يتم عرضها من قبل جميع المحللين (أو اثنين على الأقل من الثلاثة).


تحديث:
كما قرأ مؤلف PHPStan Ondřej Mirtes مقالنا وأخبرنا أن PhpStan ، مثل Psalm ، لديه موقع ويب يحتوي على "sandbox": https://phpstan.org/ . هذا مناسب جدًا لتقارير الأخطاء: أنت تعيد إنتاج الخطأ وتعطي رابطًا في GitHub.

فان


طور بواسطة Etsy. يرتكب أولا من Rasmus Lerdorf.

من بين الثلاثة ، فإن Phan هو المحلل الثابت الحقيقي الوحيد (بمعنى أنه لا ينفذ أيًا من ملفاتك - فهو يوزع قاعدة التعليمات البرمجية بالكامل ، ثم يحلل ما تقوله). حتى لتحليل العديد من الملفات في قاعدة الرموز لدينا ، فإنها تحتاج إلى حوالي 6 غيغابايت من ذاكرة الوصول العشوائي ، وتستغرق هذه العملية من أربع إلى خمس دقائق. ولكن بعد ذلك يستغرق التحليل الكامل لقاعدة التعليمات البرمجية بأكملها حوالي ست إلى سبع دقائق. للمقارنة ، يحللها المزمور في بضع دقائق. ومن PHPStan لم نتمكن من تحقيق تحليل كامل لقاعدة الشفرة بأكملها على الإطلاق لأنها تتضمن فئات.

تجربة فان ذات شقين. من ناحية ، هو المحلل الأكثر جودة وثباتًا ، فهو يجد الكثير وهناك مشاكل أقل معه عندما يكون من الضروري تحليل قاعدة التعليمات البرمجية بأكملها. من ناحية أخرى ، لديها ميزتان غير سارتين.

تحت غطاء محرك السيارة ، يستخدم فان التمديد php-ast. على ما يبدو ، هذا هو أحد الأسباب التي تجعل تحليل قاعدة التعليمات البرمجية بالكامل سريعًا نسبيًا. لكن php-ast يُظهر التمثيل الداخلي لشجرة AST كما تظهر في PHP نفسها. وفي PHP نفسها ، لا تحتوي شجرة AST على معلومات حول التعليقات الموجودة داخل الوظيفة. أي إذا كتبت شيئًا مثل:

 /** * @param int $type */ function doSomething($type) {   /** @var \My\Object $obj **/   $obj = MyFactory::createObjectByType($type);   … } 

ثم داخل شجرة AST هناك معلومات حول doSomething() الخارجية doSomething() ، ولكن لا توجد معلومات doSomething() داخل الوظيفة. وبالتالي ، فإن فان لا تعرف شيئًا عنها أيضًا. هذا هو السبب الأكثر شيوعًا false-positive في فان. هناك بعض التوصيات حول كيفية إدراج تلميحات الأدوات (من خلال السلاسل أو التأكيد) ، ولكن للأسف ، فهي مختلفة تمامًا عما اعتاد المبرمجون لدينا عليه. جزئيًا ، تمكنا من حل هذه المشكلة عن طريق كتابة مكون إضافي لـ Phan. ولكن سيتم مناقشة الإضافات أدناه.

الميزة الثانية غير السارة هي أن فان لا يحلل خصائص الأشياء جيدًا. هنا مثال:

 class A { /** * @var string|null */ private $a; public function __construct(string $a = null) {      $this->a = $a; } public function doSomething() {      if ($this->a && strpos($this->a, 'a') === 0) {          var_dump("test1");      } } } 

في هذا المثال ، سيخبرك Phan أنه في strpos يمكنك تمرير قيمة فارغة. يمكنك معرفة المزيد عن هذه المشكلة هنا: https://github.com/phan/phan/issues/204 .

الملخص على الرغم من بعض الصعوبات ، فإن فان تطور رائع ومفيد للغاية. بالإضافة إلى هذين النوعين من false-positive ، فإنه لا يخطئ تقريبًا ، أو يرتكب أخطاء ، ولكن على بعض الرموز المعقدة حقًا. لقد أحببنا أيضًا أن التهيئة موجودة في ملف PHP - وهذا يعطي بعض المرونة. يعرف Phan أيضًا كيفية العمل كخادم لغة ، لكننا لم نستخدم هذه الميزة ، نظرًا لأن PHPStorm كافٍ بالنسبة لنا.

الإضافات


يمتلك فان واجهة برمجة تطبيقات متطورة لتطوير المكونات. يمكنك إضافة الشيكات الخاصة بك ، وتحسين الاستدلال على النوع الخاص بك. تحتوي واجهة برمجة التطبيقات هذه على وثائق ، ولكن من الرائع بشكل خاص وجود مكونات إضافية تعمل بالفعل يمكن استخدامها كأمثلة.

تمكنا من كتابة ملحقين. كان الأول مخصصًا لإجراء فحص لمرة واحدة. أردنا تقييم مدى جاهزية الكود الخاص بنا لـ PHP 7.3 (على وجه الخصوص ، لمعرفة ما إذا كان يحتوي case-insensitive ثوابت case-insensitive ). كنا على يقين تقريبًا من عدم وجود مثل هذه الثوابت ، ولكن يمكن أن يحدث أي شيء خلال 12 عامًا - يجب التحقق منه. وكتبنا مكونًا إضافيًا لـ Phan يمكن أن يقسم إذا تم استخدام المعلمة الثالثة في define() .

البرنامج المساعد بسيط للغاية
 <?php declare(strict_types=1); use Phan\AST\ContextNode; use Phan\CodeBase; use Phan\Language\Context; use Phan\Language\Element\Func; use Phan\PluginV2; use Phan\PluginV2\AnalyzeFunctionCallCapability; use ast\Node; class DefineThirdParamTrue extends PluginV2 implements AnalyzeFunctionCallCapability { public function getAnalyzeFunctionCallClosures(CodeBase $code_base) : array {   $define_callback = function (       CodeBase $code_base,                  Context $context,                  Func $function,                  array $args    ) {      if (\count($args) < 3) {         return;      }       $this->emitIssue(       $code_base,      $context,      'PhanDefineCaseInsensitiv',      'Define with 3 arguments',      []      );    };    return [          'define' => $define_callback,    ]; } } return new DefineThirdParamTrue(); 



في Phan ، يمكن تعليق المكونات الإضافية المختلفة في أحداث مختلفة. على وجه الخصوص ، AnalyzeFunctionCallCapability تشغيل المكونات الإضافية مع واجهة AnalyzeFunctionCallCapability عندما يتم تحليل استدعاء دالة. في هذا المكوّن الإضافي ، صنعنا ذلك حتى عندما نسمي وظيفة define() ، تُدعى دالتنا المجهولة ، والتي تتحقق من أن التعريف define() لا define() أكثر من وسيطتين. ثم بدأنا للتو في فان ، ووجدنا جميع الأماكن التي تم فيها استدعاء define() بثلاث حجج ، case-insensitive- .

باستخدام البرنامج المساعد ، قمنا أيضًا بحل مشكلة false-positive جزئيًا عندما لا يرى Phan تلميحات PHPDoc داخل الكود.

غالبًا ما نستخدم أساليب المصنع التي تأخذ ثابتًا كمدخل وننشئ كائنًا منه. غالبًا ما يبدو الرمز كالتالي:

 /** @var \Objects\Controllers\My $Object */ $Object = \Objects\Factory::create(\Objects\Config::MY_CONTROLLER); 

لا يفهم Phan تلميحات PHPDoc ، ولكن في هذا الرمز يمكن الحصول على فئة الكائن من اسم الثابت الذي تم تمريره إلى طريقة create() . يتيح لك Phan كتابة مكون إضافي ينطلق عندما يحلل القيمة المرجعة للدالة. وباستخدام هذا البرنامج المساعد ، يمكنك إخبار المحلل عن نوع الوظيفة التي ترجعها هذه المكالمة.

مثال على هذا البرنامج المساعد هو أكثر تعقيدا. ولكن هناك مثال جيد في كود Phan في vendor/phan/phan/src/Phan/Plugin/Internal/DependentReturnTypeOverridePlugin.php.

بشكل عام ، نحن سعداء جدًا بمحلل Phan. false-positive المذكورة أعلاه تعلمنا جزئيًا (في حالات بسيطة ، مع كود بسيط) للتصفية. بعد ذلك ، أصبح فان محللًا مرجعيًا تقريبًا. ومع ذلك ، فإن الحاجة إلى تحليل قاعدة التعليمات البرمجية بالكامل (الوقت والكثير من الذاكرة) على الفور لا تزال تعقد عملية تنفيذها.

مزمور


المزمور هو تطور بواسطة Vimeo. بصراحة ، لم أكن أعرف حتى أن Vimeo يستخدم PHP حتى رأيت المزمور.

هذا المحلل هو أصغر ثلاثة لدينا. عندما قرأت الأخبار بأن Vimeo أصدرت مزمورًا ، كنت في حيرة: "لماذا تستثمر في المزمور إذا كان لديك بالفعل Phan و PHPStan؟" ولكن اتضح أن مزمور له خصائصه المفيدة.

يتبع مزمور خطى PHPStan: يمكنك أيضًا إعطائه قائمة بالملفات لتحليلها ، وسيقوم بتحليلها ، وربط الفئات التي لم يتم العثور عليها مع التحميل التلقائي. في الوقت نفسه ، يربط فقط الفئات التي لم يتم العثور عليها ، ولن يتم تضمين الملفات التي طلبنا تحليلها (وهذا يختلف عن PHPStan). يتم تخزين التكوين في ملف XML (بالنسبة لنا ، هذا على الأرجح ناقص ، ولكن ليس بالغ الأهمية).

يحتوي مزمور على موقع محمي حيث يمكنك كتابة كود PHP وتحليله. هذا مناسب جدًا لتقارير الأخطاء: أنت تعيد إنتاج الخطأ على الموقع وتعطي الرابط في GitHub. وبالمناسبة ، يصف الموقع جميع أنواع الأخطاء المحتملة. للمقارنة: في PHPStan أخطاء ليس لها أنواع ، وفي فان ، ولكن لا توجد قائمة واحدة يمكن العثور عليها.

لقد أحببنا أيضًا أنه عند إخراج الأخطاء ، يُظهر المزمور على الفور سطور الكود حيث تم العثور عليها. هذا يبسط قراءة التقارير إلى حد كبير .

ولكن ربما تكون الميزة الأكثر إثارة للاهتمام في المزمور هي علامات PHPDoc المخصصة ، والتي تتيح لك تحسين التحليل (خاصة تعريف الأنواع). نحن ندرج أكثرها إثارة للاهتمام.

@ psalm-ignore-nullable-return


يحدث أن الطريقة يمكن رسميًا إرجاع قيمة null ، ولكن تم تنظيم الرمز بالفعل بطريقة لا يحدث فيها هذا أبدًا. في هذه الحالة ، من الملائم للغاية أنه يمكنك إضافة مثل هذا التلميح PHPDoc إلى الطريقة / الوظيفة - وسيعتبر المزمور أن قيمة null لا null إرجاعها.

يوجد تلميح مماثل @psalm-ignore-falsable-return : @psalm-ignore-falsable-return .

أنواع الإغلاق


إذا كنت مهتمًا بالبرمجة الوظيفية ، فربما لاحظت أنه غالبًا ما توجد وظيفة يمكن أن ترجع وظيفة أخرى أو تأخذ بعض الوظائف كمعلمة. في PHP ، يمكن أن يكون هذا النمط مربكًا للغاية لزملائك ، وأحد الأسباب هو أن PHP ليس لديه معايير لتوثيق مثل هذه الوظائف. على سبيل المثال:

 function my_filter(array $ar, \Closure $func) { … } 

كيف يمكن للمبرمج فهم الواجهة التي تحتوي عليها الوظيفة في المعلمة الثانية؟ ما هي المعلمات التي يجب أن تأخذها؟ ماذا يجب أن تعود؟

المزمور يدعم بناء الجملة لوصف الوظائف في PHPDoc:

 /** * @param array $ar * @psalm-param Closure(int):bool $func */ function my_filter(array $ar, \Closure $func) { … } 

مع هذا الوصف ، من الواضح بالفعل أنك بحاجة إلى تمرير وظيفة مجهولة الهوية إلى my_filter ، والتي ستقبل int my_filter . وبالطبع ، سيتحقق مزمور من أن لديك مثل هذه الوظيفة التي تم تمريرها بالضبط في التعليمات البرمجية الخاصة بك.

التعدادات


افترض أن لديك وظيفة تأخذ معلمة سلسلة ، ويمكنك فقط تمرير سلاسل معينة هناك:

 function isYes(string $yes_or_no) : bool {     $yes_or_no = strtolower($yes_or_no)     switch($yes_or_no)  {           case 'yes':                 return true;          case 'no':                 return false;           default:                throw new \InvalidArgumentException(…);     } } 

يسمح لك المزمور بوصف معلمة هذه الوظيفة كما يلي:

 /** @psalm-param 'Yes'|'No' $yes_or_no **/ function isYes(string $yes_or_no) : bool { … } 

في هذه الحالة ، سيحاول المزمور فهم القيم المحددة التي يتم تمريرها إلى هذه الوظيفة ، ويرمي الأخطاء إذا كانت هناك قيم أخرى غير Yes ولا No

اقرأ المزيد عن التعداد هنا .

اكتب الأسماء المستعارة


في وقت سابق من وصف array shapes ذكرت أنه على الرغم من أن المحللون يسمحون لك بوصف بنية المصفوفات ، إلا أنه ليس من المناسب استخدامه ، حيث يجب نسخ وصف الصفيف في أماكن مختلفة. الحل الصحيح ، بالطبع ، هو استخدام الفئات بدلاً من المصفوفات. ولكن في حالة سنوات عديدة من الإرث ، هذا ليس ممكنًا دائمًا.

, , , :

  • ;
  • closure;
  • union- (, );
  • enum.

, , PHPDoc , , . Psalm . alias PHPDoc alias . , : PHP-. . , Psalm.

Generics aka templates


. , :

 function identity($x) { return $x; } 

? ? ?

, , , — mixed , .

mixed — . , . , identity() / , : , . -. , :

 $i = 5; // int $y = identity($i); 

(int) , , $y ( int ).

? Psalm PHPDoc-:

 /** * @template T * @psalm-param T $x * @psalm-return T */ function identity($x) { $return $x; } 

templates Psalm , / .

Psalm templates:

vendor/vimeo/psalm/src/Psalm/Stubs/CoreGenericFunctions.php ;
vendor/vimeo/psalm/src/Psalm/Stubs/CoreGenericClasses.php .

Phan, : https://github.com/phan/phan/wiki/Generic-Types .

, Psalm . , «» . , Psalm , , Phan PHPStan. .

PHPStorm


: , . , , .

. Phan, language server. PHPStorm, , .

, , PHPStorm ( ), . — Php Inspections (EA Extended). — , , . , . , scopes - scopes.

, deep-assoc-completion . .

Badoo


?

, .

, . , , git diff / , , () . , .

, : - git diff . . , . . , , , , .

, , :



false-positive . , , Phan , , . , - Phan , , .

QA


:



— , , , . :

  • 100% ( , );
  • , code review;
  • , .

strict types . , strict types , :

  • , strict types , ;
  • , (, , );
  • , PHP (, union types , PHP);
  • strict types , .

:


, . .

-, , , - , .

-, , — , , PHPDoc. — .

-, . , - , PHPDoc. :)

, , . , .

Source: https://habr.com/ru/post/ar426605/


All Articles