ظهرت ميزة قاتلة في أداة التحليل الثابت NoVerify : طريقة تعريف لوصف عمليات التفتيش التي لا تتطلب برنامج Go وتجميع التعليمات البرمجية.
لإثارة اهتمامك ، سأريك وصفًا لتفتيش بسيط ولكنه مفيد:
$x && $x;
يعثر هذا الفحص على جميع التعبيرات المنطقية &&
حيث يكون المعامل الأيسر والأيمن متطابقين.
NoVerify هو محلل ثابت ل PHP مكتوب في الذهاب . يمكنك أن تقرأ عنها في مقالة " NoVerify: Linter for PHP من فريق فكونتاكتي ". وفي هذا الاستعراض سوف أتحدث عن الوظيفة الجديدة وكيف توصلنا إليها.

الشروط
عندما تحتاج حتى إلى عملية تحقق جديدة بسيطة لكتابة بضع عشرات من سطور التعليمات البرمجية على Go ، تبدأ في التساؤل: هل هذا ممكن على خلاف ذلك؟
في Go ، لقد كتبنا الاستدلال النوعي ، وخط الأنابيب بالكامل للنت ، وذاكرة التخزين المؤقت للبيانات التعريفية ، والعديد من العناصر المهمة الأخرى التي بدونها NoVerify مستحيل. هذه المكونات فريدة ، لكن مهام مثل "حظر استدعاء دالة X مع مجموعة من الوسائط Y" لا. فقط لهذه المهام البسيطة تمت إضافة آلية القواعد الديناميكية.
تسمح لك القواعد الديناميكية بفصل الأجزاء الداخلية المعقدة عن حل المشكلات النموذجية. يمكن تخزين ملف التعريف وإصداره بشكل منفصل - يمكن تحريره بواسطة أشخاص لا علاقة لهم بتطوير NoVerify نفسه. تطبق كل قاعدة عملية فحص للرموز (والتي سوف نسميها في بعض الأحيان للتحقق).
نعم ، إذا كانت لدينا لغة لوصف هذه القواعد ، فيمكنك دائمًا كتابة قالب غير صحيح بشكل واضح أو تجاهل بعض القيود على الأنواع - وهذا يؤدي إلى إيجابيات خاطئة. ومع ذلك ، لا يتم إدخال سباق البيانات أو إلغاء nil
المؤشر من خلال لغة القواعد.
لغة وصف القالب
لغة الوصف متوافقة مع صيغة PHP. هذا يبسط دراسته ، ويجعل من الممكن أيضًا تحرير ملفات القواعد باستخدام نفس PhpStorm.
في بداية ملف القواعد ، يوصى بإدراج توجيه يهدئ بيئة IDE المفضلة لديك:
<?php
تجربتي الأولى في بناء الجملة والمرشحات المحتملة للقوالب كانت phpgrep . قد يكون ذلك مفيدًا من تلقاء نفسه ، ولكن داخل NoVerify أصبح الأمر أكثر إثارة للاهتمام ، لأنه الآن لديه إمكانية الوصول إلى كتابة المعلومات.
لقد جرب بعض زملائي بالفعل phpgrep في عملهم ، وكانت هذه حجة أخرى لصالح اختيار بناء جملة من هذا القبيل.
Phpgrep نفسها عبارة عن تكيف gogrep لـ PHP (قد تكون مهتمًا أيضًا بـ cgrep ). باستخدام هذا البرنامج ، يمكنك البحث عن رمز من خلال قوالب بناء الجملة .
سيكون البديل هو البحث الهيكلي واستبدال (SSR) بناء جملة من PhpStorm. المزايا واضحة - هذا تنسيق موجود ، لكنني اكتشفت هذه الميزة بعد أن قمت بتنفيذ phpgrep. يمكنك بالطبع تقديم تفسير تقني: فهناك بناء جملة لا يتوافق مع PHP ولن يقوم المحلل اللغوي لدينا بإتقانه ، ولكن تم اكتشاف هذا السبب "الحقيقي" المقنع بعد كتابة الدراجة.
في الواقع ، كان هناك خيار آخر
قد يكون مطلوبًا عرض قالب برمز PHP تقريبًا واحد لواحد - أو الانتقال في الاتجاه الآخر: اختراع لغة جديدة ، على سبيل المثال ، باستخدام بناء جملة S-expression .
PHP-like Lisp-like ----------------------------- $x = $y | (expr = $x $y) fn($x, 1) | (expr call fn $x 1) : (or (expr == (type string (expr)) (expr)) (expr == (expr) (type string (expr))))
في النهاية ، اعتقدت أن إمكانية قراءة القوالب لا تزال مهمة ، ويمكننا إضافة عوامل تصفية من خلال سمات phpdoc.
clang-query مثال على فكرة مماثلة ، لكنه يستخدم بناء جملة أكثر تقليدية.
نقوم بإنشاء وتشغيل التشخيص الخاصة بنا!
دعنا نحاول تنفيذ تشخيصاتنا الجديدة للمحلل.
للقيام بذلك ، تحتاج إلى تثبيت NoVerify. خذ الإصدار الثنائي إذا لم يكن لديك Go-toolchain في النظام (إذا كان لديك واحدة ، فيمكنك ترجمة كل شيء من المصدر).
بيان المشكلة
يحتوي PHP على العديد من الوظائف المثيرة للاهتمام ، واحدة منها parse_str . توقيعها:
سوف تفهم ما هو الخطأ هنا إذا نظرت إلى هذا المثال من الوثائق:
$str = "first=value&arr[]=foo+bar&arr[]=baz"; parse_str($str); echo $first;
ط ط ط ، كانت المعلمات من السلسلة في النطاق الحالي. لتجنب ذلك ، سنطلب في اختبارنا الجديد استخدام المعلمة الثانية للدالة ، $result
، بحيث تتم كتابة النتيجة في هذه المجموعة.
إنشاء التشخيصات الخاصة بك
قم myrules.php
ملف myrules.php
:
<?php parse_str($_);
ملف القاعدة بشكل عام عبارة عن قائمة من التعبيرات في المستوى العلوي ، يتم تفسير كل منها على أنه قالب phpgrep. ومن المتوقع تعليق phpdoc خاص لكل قالب من هذا القبيل. مطلوب سمة واحدة فقط - فئة خطأ مع نص تحذير.
هناك الآن أربعة مستويات في المجموع: error
، warning
، info
maybe
. الأولان مهمان: سيعود linter إلى رمز غير صفري بعد التنفيذ في حالة عمل أحد القواعد الأساسية على الأقل. بعد السمة نفسها ، يوجد نص تحذير سيتم إصداره بواسطة linter في حالة تشغيل القالب.
يستخدم القالب الذي كتبناه $_
- هذا متغير قالب غير مسمى. يمكن أن نسميها ، على سبيل المثال ، $x
، ولكن بما أننا لا نقوم بأي شيء باستخدام هذا المتغير ، فيمكننا أن نطلق عليه اسم "فارغ". الفرق بين متغيرات القالب ومتغيرات PHP هو أن السابق يتزامن مع أي تعبير على الإطلاق ، وليس فقط مع متغير "حرفي". هذا مناسب: نحتاج غالبًا للبحث عن تعبيرات غير معروفة ، بدلاً من متغيرات محددة.
بدء تشخيص جديد
قم بإنشاء ملف اختبار صغير لتصحيح الأخطاء ، test.php
:
<?php function f($x) { parse_str($x);
بعد ذلك ، قم بتشغيل NoVerify مع قواعدنا في هذا الملف:
$ noverify -rules myrules.php test.php
سيظهر تحذيرنا بشيء من هذا القبيل:
WARNING myrules.php:4: parse_str without second argument at test.php:4 parse_str($x); ^^^^^^^^^^^^^
اسم الفحص الافتراضي هو اسم ملف القواعد والخط الذي يحدد عملية التحقق هذه. في حالتنا ، هذا هو myrules.php:4
.
يمكنك تعيين اسمك باستخدام @name <name>
.
مثال الاسم
parse_str($_);
WARNING parseStrResult: parse_str without second argument at test.php:4 parse_str($x); ^^^^^^^^^^^^^
تخضع القواعد المسماة لقوانين التشخيصات الأخرى:
- يمكن تعطيلها عن طريق
-exclude-checks
- يمكن إعادة تعريف مستوى
-critical
عبر- -critical
العمل مع الأنواع
المثال السابق مفيد لـ hello world - لكن غالبًا ما نحتاج إلى معرفة أنواع التعبيرات من أجل تقليل عدد عمليات التشخيص
على سبيل المثال ، بالنسبة لوظيفة in_array ، نطلب الوسيطة $strict=true
عندما تكون الوسيطة الأولى ( $needle
) من نوع السلسلة.
لهذا لدينا مرشحات النتيجة.
أحد هذه المرشحات هو @type <type> <var>
. يسمح لك بتجاهل كل ما لا يتناسب مع الأنواع المذكورة.
in_array($needle, $_);
هنا قدمنا اسم الوسيطة الأولى لاستدعاء in_array
لربط مرشح نوع به. سيتم إصدار تحذير فقط عندما يكون نوع $needle
هو string
.
يمكن دمج مجموعات المرشحات مع عامل التشغيل @or
:
$x == $y;
في المثال أعلاه ، لن يتطابق النقش إلا مع تعبيرات ==
، حيث يكون أي من المعاملات من النوع type. يمكن افتراض أنه بدون @or
يتم دمج جميع المرشحات من خلال @and
، ولكن لا يلزم الإشارة إلى ذلك بوضوح.
الحد من نطاق التشخيص
لكل اختبار ، يمكنك تحديد @scope <name>
:
@scope all
- القيمة الافتراضية ، يعمل التحقق من الصحة في كل مكان ؛@scope root
- الإطلاق فقط في المستوى الأعلى ؛@scope local
- تشغيل فقط داخل وظائف وطرق.
لنفترض أننا نريد الإبلاغ عن return
خارج الجسم الوظيفي. في PHP ، يكون ذلك منطقيًا في بعض الأحيان - على سبيل المثال ، عندما يتم توصيل ملف من دالة ... ولكن في هذه المقالة ندين هذا.
return $_;
لنرى كيف ستتصرف هذه القاعدة:
<?php function f() { return "OK"; } return "NOT OK";
وبالمثل ، يمكنك تقديم طلب لاستخدام *_once
بدلاً من require
include
:
require $_; include $_;
الآن عند مطابقة الأنماط ، لا تؤخذ الأقواس في الاعتبار بشكل متسق تمامًا. لن يجد النموذج (($x))
"كل التعبيرات بين أقواس مزدوجة" ، ولكن ببساطة أي تعبيرات ، متجاهلاً الأقواس. ومع ذلك ، يتصرف $x+$y*$z
و ($x+$y)*$z
كما ينبغي. تأتي هذه الميزة من صعوبات العمل مع الرموز (
و )
، ولكن هناك فرصة لاستعادة الطلب في أحد الإصدارات التالية.
قوالب التجميع
عندما يظهر تكرار تعليقات phpdoc على القوالب ، فإن القدرة على الجمع بين القوالب تنقذ.
مثال بسيط لإظهار:
الآن تخيل كيف سيكون من غير اللائق وصف قاعدة في المثال التالي دون هذه الميزة!
{ $x > $y; $x < $y; $x >= $y; $x <= $y; $x == $y; }
شكل التسجيل المحدد في المقال هو مجرد واحد من الخيارات المقترحة. إذا كنت ترغب في المشاركة في الاختيار ، فلديك فرصة كهذه: تحتاج إلى وضع +1 لتلك العروض التي تفضلها أكثر من غيرها. لمزيد من التفاصيل ، انقر هنا .
كيف يتم دمج القواعد الديناميكية

في وقت الإطلاق ، يحاول NoVerify العثور على ملف القواعد المحدد في وسيطة rules
.
بعد ذلك ، يتم تحليل هذا الملف كبرنامج نصي PHP عادي ، ومن AST الناتج ، يتم جمع مجموعة من كائنات القاعدة مع قوالب phpgrep المرتبطة بها.
ثم يبدأ المحلل العمل وفقًا للمخطط المعتاد - والفرق الوحيد هو أنه بالنسبة لبعض أقسام الكود المحددة ، يبدأ مجموعة من القواعد المرتبطة. إذا تم تشغيل القاعدة ، فسيتم عرض تحذير.
يعتبر النجاح بمثابة مطابقة لقالب phpgrep ومرة واحدة على الأقل من مجموعات المرشحات (مفصولة بواسطة @or
).
في هذه المرحلة ، لا تبطئ آلية القواعد بشكل كبير تشغيل اللنت ، حتى لو كان هناك الكثير من القواعد الديناميكية.
مطابقة الخوارزمية
مع النهج الساذج ، لكل عقدة AST ، نحتاج إلى تطبيق جميع القواعد الديناميكية. هذا تطبيق غير فعال للغاية ، لأن معظم العمل سيتم دون جدوى: تحتوي العديد من القوالب على بادئة محددة يمكننا من خلالها تجميع القواعد.
هذا مشابه لفكرة المطابقة المتوازية ، لكن بدلاً من بناء NFA بأمانة ، فإننا "نوازن" الخطوة الأولى فقط من الحسابات.
النظر في هذا مع مثال مع ثلاثة قواعد:
$_ ? $x : $x; explode("", ${"*"}); if ($_);
إذا كان لدينا عناصر N وقواعد M ، مع اتباع نهج ساذج لدينا عمليات N * M لأداء. من الناحية النظرية ، يمكن تقليل هذا التعقيد إلى خطي والحصول على O(N)
- إذا قمت بدمج جميع الأنماط في نموذج واحد وقمت بإجراء المطابقة كما تفعل ، على سبيل المثال ، حزمة regexp من Go.
ومع ذلك ، في الممارسة العملية ، لقد ركزت حتى الآن على التنفيذ الجزئي لهذا النهج. سوف يسمح بتقسيم القواعد من الملف أعلاه إلى ثلاث فئات ، وعناصر AST التي لا تتوافق معها أي قاعدة ، لتعيين فئة رابعة فارغة. لهذا السبب ، لا يتم تنفيذ أكثر من قاعدة لكل عنصر.
إذا كان لدينا الآلاف من القواعد وسوف نشعر بحدوث تباطؤ كبير ، فسيتم الانتهاء من الخوارزمية. في غضون ذلك ، فإن بساطة الحل والتسارع الناتج يناسبني.
بناء الجملة الحالي يكرر @var
و @var
، لكننا قد نحتاج إلى عوامل تشغيل جديدة ، على سبيل المثال ، "النوع غير متساوي". تخيل كيف قد تبدو.
لدينا أولويتان مهمتان على الأقل:
- بناء الجملة المقروءة والموجزة للتعليقات التوضيحية.
- أعلى دعم ممكن من IDE دون بذل جهد إضافي.
هناك مكون إضافي لـ php-annotations لـ PhpStorm ، يضيف الإكمال التلقائي ، والانتقال إلى فئات التعليقات التوضيحية ، وفائدة أخرى للعمل مع تعليقات phpdoc.
الأولوية (2) في الممارسة العملية تعني أنك تتخذ قرارات لا تتعارض مع توقعات IDE والمكونات الإضافية. على سبيل المثال ، يمكنك إنشاء تعليقات توضيحية بتنسيق يمكن لمكوّن php-annotations التعرف عليه:
class Filter { public $value; public $type; public $text; }
عندئذٍ سيبدو تطبيق مرشح على أنواع مثل هذا:
@Type($needle, eq=string) @Type($x, not_eq=Foo)
يمكن للمستخدمين الانتقال إلى تعريف Filter
، وسيُطلب منهم قائمة بالمعلمات المحتملة (النوع / النص / إلخ).
طرق التسجيل البديلة ، والتي اقترح بعضها من قبل الزملاء:
@type string $needle @type !Foo $x @type $needle == string @type $x != Foo @type(==) string $needle @type(!=) Foo $x @type($needle) == string @type($x) != Foo @filter type($needle) == string @filter type($x) != Foo
ثم انصرفنا قليلاً ونسينا أن كل شيء كان داخل phpdoc ، وظهر هذا:
(eq string (typeof $needle)) (neq Foo (typeof $x))
على الرغم من أن الخيار مع تسجيل postfix للمتعة كما بدا. يمكن تسمية لغة لوصف قيود النوع والقيمة السادسة:
@eval string $needle typeof = @eval Foo $x typeof <>
البحث عن الخيار الأفضل لم ينته بعد ...
مقارنة التمدد مع فان
كأحد مزايا Phan ، تشير مقالة " التحليل الثابت لرمز PHP باستخدام مثال PHPStan و Phan و Psalm " إلى القابلية للتوسعة.
هنا ما تم تنفيذه في نموذج المساعد:
أردنا تقييم مدى جاهزية الكود الخاص بنا لـ PHP 7.3 (على وجه الخصوص ، لمعرفة ما إذا كان به ثوابت غير حساسة لحالة الأحرف). كنا متأكدين تقريبًا من عدم وجود مثل هذه الثوابت ، لكن أي شيء يمكن أن يحدث خلال 12 عامًا - يجب التحقق منه. وكتبنا مكونًا إضافيًا لـ Phan من شأنه أن يقسم إذا تم استخدام المعلمة الثالثة في define ().
هذه هي الطريقة التي يبدو بها رمز المكون الإضافي (يتم تحسين التنسيق للعرض):
<?php 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) { $def = function(CodeBase $cb, Context $ctx, Func $fn, $args) { if (count($args) < 3) { return; } $this->emitIssue( $cb, $ctx, 'PhanDefineCaseInsensitiv', 'define with 3 arguments', [] ); }; return ['define' => $def]; } } return new DefineThirdParamTrue();
وهنا كيف يمكن القيام بذلك في NoVerify:
<?php define($_, $_, $_);
أردنا أن نحقق نفس النتيجة تقريبًا - بحيث يمكن القيام بأشياء تافهة قدر الإمكان.
استنتاج
روابط ، مواد مفيدة
يتم جمع روابط مهمة هنا ، بعضها قد يكون مذكورًا بالفعل في المقال ، ولكن من أجل الوضوح والراحة ، قمت بجمعها في مكان واحد.
إذا كنت بحاجة إلى المزيد من الأمثلة على القواعد التي يمكن تنفيذها ، يمكنك إلقاء نظرة خاطفة على اختبارات NoVerify .