الجزء المفضل لدي في تحليل الكود الثابت هو فرضيات حول الأخطاء المحتملة في الكود ثم التحقق منها.
مثال الفرضية:
strpos .
ولكن هناك فرصة حتى أنه على بضعة ملايين سطر من التعليمات البرمجية ، فإن مثل هذا التشخيص لن "يطلق النار" ، لذلك لا ترغب في قضاء الكثير من الوقت على فرضيات غير ناجحة.
اليوم سأوضح كيفية إجراء أبسط تحليل ثابت باستخدام أداة phpgrep دون كتابة التعليمات البرمجية.
الشروط
منذ عدة أشهر حتى الآن ، كنت أؤيد برنامج NoVerify PHP linter (اقرأ عن ذلك في مقالة NoVerify: Linter for PHP من فريق VKontakte ).
من وقت لآخر ، تظهر أفكار التشخيص الجديدة في الفريق. يمكن أن يكون هناك العديد من الأفكار ، لكنني أرغب في التحقق من كل شيء ، خاصة إذا كان التحقق المقترح يهدف إلى تحديد العيوب الحرجة.
في السابق ، كنت أقوم بنشاط بتطوير برنامج go-الناقد وكان الموقف مشابهًا ، مع وجود الفرق الوحيد في أن أكواد المصدر قد تم تحليلها في Go ، وليس في PHP. عندما اكتشفت فائدة gogrep ، انقلب العالم رأسًا على عقب. كما يوحي الاسم ، تحتوي هذه الأداة المساعدة على شيء مشترك مع grep ، لا يتم إجراء البحث إلا من خلال التعبيرات المعتادة ، ولكن من خلال أنماط بناء الجملة (سأشرح لاحقًا ما يعنيه هذا).
لم أكن أرغب في العيش من دون خربة ذكية ، لذلك في إحدى الأمسيات قررت الجلوس وأكتب phpgrep
.
حالة تحليلها
لتكون متعة ، ونحن على الفور تغمر أنفسنا في التطبيق. سنقوم بتحليل مجموعة صغيرة من مشاريع PHP المشهورة والكبيرة المتاحة على جيثب.
تضمنت المجموعة لدينا المشاريع التالية:
بالنسبة للأشخاص الذين يخططون لما نخطط له ، هذه مجموعة شهية للغاية.
لذلك دعونا نذهب!
استخدام الواجب كتعبير
إذا تم استخدام الواجب كتعبير ، علاوة على ذلك:
- يتوقع السياق نتيجة العملية المنطقية (الشرط المنطقي) و
- الجانب الأيمن من التعبير ليس له أي آثار جانبية وهو ثابت ،
إنه على الأرجح خطأ في الشفرة.
بادئ ذي بدء ، دعنا نأخذ الإنشاءات التالية لـ "السياق المنطقي":
- التعبير داخل "
if ($cond)
". - حالة العامل الثلاثي هي: "
$cond ? $x : $y
". - شروط استمرار الحلقات "
while ($cond)
" و " for ($init; $cond; $post)
".
على الجانب الأيمن من المهمة ، نتوقع ثوابت أو حرفية.
لماذا نحتاج إلى مثل هذه القيود؟ لنبدأ بـ (1):
نرى هنا 4 أنماط ، والفرق الوحيد بينها هو التعبير المخصص (RHS). لنبدأ مع أول واحد.
القالب " if ($_ = []) $_
" يلتقط if
، الذي يحتوي على صفيف فارغ مخصص لأي تعبير. $_
يطابق أي تعبير أو عبارة.
(RHS) | if ($_ = []) $_ | | | if', , {} LHS
تستخدم الأمثلة التالية مجموعات const و str و num الأكثر تعقيدًا. على عكس $_
يصفون القيود المفروضة على العمليات المتوافقة.
const
هو ثابت مسمى أو ثابت فئة.str
عبارة عن سلسلة حرفية من أي نوع.num
هو حرفي رقمي من أي نوع.
هذه الأنماط كافية لتحقيق العديد من العمليات في القضية.
oodle moodle / blocks / rss_client / viewfeed.php # L37 :
if ($courseid = SITEID) { $courseid = 0; }
المشغل الثاني في moodle هو التبعية ADOdb . في مكتبة المنبع ، لا تزال المشكلة موجودة.
⎆ ADOdb / drivers / adodb-odbtp.inc.php # L741 :

يوجد الكثير في هذا الجزء ، ولكن بالنسبة لنا فقط السطر الأول هو المناسب. بدلاً من مقارنة حقل databaseType
، نقوم بتنفيذ الواجب ونذهب دائمًا إلى داخل الشرط.
مكان آخر مثير للاهتمام حيث نريد تنفيذ إجراءات فقط للسجلات "الصحيحة" ، ولكن بدلاً من ذلك ، قم بتنفيذها دائمًا ، علاوة على ذلك ، ضع علامة على أي سجل على أنه صحيح!
oodle moodle / question / format / blackboard_six / formatqti.php # L598 :
قائمة موسعة من القوالب لهذا الاختيار لنكرر ما تعلمناه:
- تشبه القوالب رمز php الذي يجده.
$_
تعني أي شيء. يمكنك مقارنة مع .
في التعبيرات العادية.${"<class>"}
مثل $_
مع تقييد نوع عنصر AST.
يجدر أيضًا التأكيد على أن كل شيء ما عدا المتغيرات يتم تعيينه حرفيًا. هذا يعني أن النموذج " array(1, 2 + 3)
" لن يكون راضيا إلا عن طريق الكود المطابق في التركيب النحوي (المسافات لا تؤثر). من ناحية أخرى ، فإن النموذج " array($_, $_)
" يلبي أي صفيف مكون من عنصرين حرفيًا.
مقارنة تعبير عن نفسك
إن الحاجة لمقارنة شيء ما مع نفسه أمر نادر للغاية. قد يكون فحص NaN
، لكن على الأقل نصف الوقت هو خطأ في النسخ / اللصق.
ik Wikia / app / extensions / SemanticDrilldown / include / SD_FilterValue.php # L103 :
if ( $fv1->month == $fv1->month ) return 0;
إلى اليمين يجب أن يكون " $fv2->month
".
للتعبير عن الأجزاء المكررة في قالب ما ، نستخدم متغيرات بأسماء أخرى غير " _
". تشبه آلية التكرار في نمط ما الروابط الخلفية في التعبيرات العادية.
سيكون النموذج " $x == $x
" هو ما يجده المثال أعلاه. بدلاً من " x
" ، يمكن استخدام أي اسم. من المهم فقط أن تكون الأسماء متطابقة. متغيرات القالب التي لها أسماء مميزة ليست مطلوبة أن يكون لها نفس المحتوى عند الالتقاط.
تم العثور على المثال التالي باستخدام " $x <= $x
".
⎆ دروبال / كور / وحدات / طرق عرض / اختبارات / src / وحدة / ViewsDataTest.php # L166 :
$prev = $base_tables[$base_tables_keys[$i - 1]]; $current = $base_tables[$base_tables_keys[$i]]; $this->assertTrue( $prev['weight'] <= $current['weight'] && $prev['title'] <= $prev['title'],
مكررة subexpressions
الآن بعد أن علمنا بإمكانيات التعبير الفرعي المتكرر ، يمكننا إعداد العديد من الأنماط المثيرة للاهتمام.
واحدة من المفضلة هي " $_ ? $x : $x
".
هذا هو المشغل الثلاثي مع فروع صحيحة / خاطئة متطابقة.
oom جملة جملة / مكتبات / src / المستخدم / UserHelper.php # L522 :
return ($show_encrypt) ? '{SHA256}' . $encrypted : '{SHA256}' . $encrypted;
يتم تكرار كلا الفرعين ، مما يشير إلى وجود مشكلة محتملة في الكود. إذا نظرنا إلى الكود الموجود حوله ، يمكننا فهم ما كان ينبغي أن يكون بدلاً من ذلك. من أجل سهولة القراءة ، قمت بقص جزء من الكود وقمت بتقليل اسم المتغير $encrypted
إلى $enc
.
case 'crypt-blowfish': return ($show_encrypt ? '{crypt}' : '') . crypt($plaintext, $salt); case 'md5-base64': return ($show_encrypt) ? '{MD5}' . $enc : $enc; case 'ssha': return ($show_encrypt) ? '{SSHA}' . $enc : $enc; case 'smd5': return ($show_encrypt) ? '{SMD5}' . $enc : $enc; case 'sha256': return ($show_encrypt) ? '{SHA256}' . $enc : '{SHA256}' . $enc; default: return ($show_encrypt) ? '{MD5}' . $enc : $enc;
أراهن أن الكود يحتاج إلى التصحيح التالي:
- ($show_encrypt) ? '{SHA256}' . $encrypted : '{SHA256}' . $encrypted; + ($show_encrypt) ? '{SHA256}' . $encrypted : $encrypted;
أولويات العملية الخطرة في PHP
من بين الاحتياطات الجيدة في PHP استخدام مجموعات التجميع أينما كان من المهم أن يكون لديك الترتيب الصحيح للحسابات.
في العديد من لغات البرمجة ، يكون التعبير " x & mask != 0
" ذا معنى حدسي. إذا وصف mask
بعض الشيء ، فإن هذا الكود يتحقق من أن هذه البتة في x
لا تساوي الصفر. لسوء الحظ ، سيتم حساب هذا التعبير لـ PHP مثل هذا: " x & (mask != 0)
" ، وهو دائمًا ما لا تحتاجه.
وورد ، جملة ومودل استخدام SimplePie .
⎆ SimplePie / library / SimplePie / Locator.php # L254
⎆ SimplePie / library / SimplePie / Locator.php # L384
⎆ SimplePie / library / SimplePie / Locator.php # L412
⎆ SimplePie / library / SimplePie / Sanitize.php # L349
⎆ SimplePie / library / SimplePie.php # L1634
$feed->method & SIMPLEPIE_FILE_SOURCE_REMOTE === 0
SIMPLEPIE_FILE_SOURCE_REMOTE
تعريف SIMPLEPIE_FILE_SOURCE_REMOTE
على أنه 1
، لذلك سيكون التعبير مكافئًا لـ:
$feed->method & (1 === 0) // => $feed->method & false
متابعة موضوع أولويات العملية غير المتوقعة ، يمكنك أن تقرأ عن المشغل الثلاثي في PHP . على هابر ، تم تكريس المقالة لها: ترتيب إعدام المشغل الثلاثي .
هل من الممكن العثور على مثل هذه الأماكن باستخدام phpgrep
؟ الجواب نعم !
phpgrep . '$_ == $_ ? $_ : $_ ? $_ : $_' phpgrep . '$_ != $_ ? $_ : $_ ? $_ : $_'
فوائد التحقق من التعبير المنتظم
ik Wikia / التطبيق / الصيانة / wikia / updateCentralInterwiki.inc # L95 :
if ( preg_match( '/(wowwiki.com|wikia.com|falloutvault.com)/', $url ) ) { $local = 1; } else { $local = 0; }
وفقًا لتصور مؤلف الشفرة ، نتحقق من عنوان URL لمصادفة أحد الخيارات الثلاثة. آسف رمز .
غير محمية ، مما سيؤدي إلى حقيقة أنه بدلاً من falloutvault.com
يمكننا الحصول على falloutvaultxcom
على أي مجال واجتياز الاختبار.

هذا ليس خطأ PHP محدد. في أي تطبيق يتم فيه التحقق من الصحة من خلال تعبيرات منتظمة وتكون حرف التعريف جزءًا من السلسلة التي يتم التحقق منها ، هناك خطر من نسيان الهروب عند الحاجة إليه والحصول على ثغرة أمنية.
يمكنك العثور على مثل هذه الأماكن عن طريق تشغيل phpgrep
:
phpgrep . 'preg_match(${"pat:str"}, ${"*"})' 'pat~[^\\]\.(com|ru|net|org)\b'
نعرض sub pattern المسماة pat
، والتي تلتقط أي سلسلة حرفية ، ثم نطبق مرشحًا من التعبير العادي عليها.
المرشحات يمكن تطبيقها على أي متغير القالب. بالإضافة إلى التعبيرات العادية ، هناك أيضًا عوامل تشغيل هيكلية =
و !=
. يمكن العثور على قائمة كاملة في الوثائق .
${"*"}
على عدد تعسفي من أي وسيطات ، لذلك لا داعي للقلق بشأن المعلمات الاختيارية لوظيفة preg_match
.
مفاتيح مكررة في مجموعة الحرفية
في PHP ، لن تتلقى أي تحذير في حالة تنفيذ هذا الرمز:
<?php var_dump(['a' => 1, 'a' => 2]);
يمكننا العثور على مثل هذه المصفوفات باستخدام phpgrep
:
[${"*"}, $k => $_, ${"*"}, $k => $_, ${"*"}]
يمكن فك تشفير هذا النمط على النحو التالي: "صفيف حرفي به مفتاحان متطابقان على الأقل في وضع تعسفي." التعبيرات ${"*"}
تساعدنا على وصف "الموضع التعسفي" ، مما يسمح لعناصر 0-N قبل ، وبين ، وبعد المفاتيح التي تهمنا.
ik Wikia / app / extensions / wikia / WikiaMiniUpload / WikiaMiniUpload_body.php # L23 :
$script_a = [ 'wmu_back' => wfMessage( 'wmu_back' )->escaped(), 'wmu_back' => wfMessage( 'wmu_back' )->escaped(),
في هذه الحالة ، ليس هذا خطأ فادحًا ، لكنني أعرف الحالات التي يكون فيها تكرار المفاتيح في المصفوفات الكبيرة (100+ عنصر) سلوكًا غير متوقع على الأقل حيث تداخل أحد المفاتيح مع قيمة الآخر.
بهذا نختتم رحلتنا القصيرة بأمثلة. إذا كنت تريد المزيد ، في نهاية المقالة يصف كيفية الحصول على جميع النتائج.
ما هو phpgrep؟
يستخدم معظم المحررين و IDEs البحث عن نص عادي للبحث عن الكود (إذا لم يكن البحث عن شخصية خاصة مثل فئة أو متغير) - بمعنى آخر ، شيء مثل grep.
قمت بإدخال " $x
" ، ابحث عن " $x
". قد تكون التعبيرات العادية متاحة لك ، ثم يمكنك في الواقع محاولة تحليل كود PHP مع النظامي. أحيانًا يكون هذا الأمر مفيدًا إذا كنت تبحث عن شيء محدد وبسيط تمامًا - على سبيل المثال ، "أي متغير مع بعض اللواحق." ولكن إذا كان هذا المتغير ذو اللاحقة جزءًا من تعبير مركب آخر ، فستظهر صعوبات.
phpgrep عبارة عن أداة للبحث المريح عن كود PHP ، والذي يسمح لك بالبحث ليس باستخدام القواعد النظامية الموجهة للنص ، ولكن باستخدام القوالب المدركة لبناء الجملة.
علم بناء الجملة يعني أن لغة القالب تعكس اللغة المستهدفة ، ولا تعمل على أحرف فردية ، كما تفعل التعبيرات العادية. نحن أيضًا لا نحدث أي فرق قبل تنسيق الكود ، فهيكله مهم فقط.
محتوى اختياري: بداية سريعةبداية سريعة
تركيب
هناك تصميمات جاهزة للإصدار لـ amd64 لنظامي التشغيل Linux و Windows ، ولكن إذا كان لديك Go Go ، فهناك أمر واحد يكفي للحصول على ثنائي جديد للنظام الأساسي الخاص بك:
go get -v github.com/quasilyte/phpgrep/cmd/phpgrep
إذا كان $GOPATH/bin
في نظام $PATH
، phpgrep
الأمر phpgrep
الفور. للتحقق من ذلك ، حاول تشغيل الأمر باستخدام المعلمة -help
:
phpgrep -help
إذا لم يحدث أي شيء ، ابحث عن المكان الذي قام فيه Go بتثبيت الثنائي وإضافته إلى متغير بيئة $PATH
.
طريقة قديمة وموثوقة للنظر في $GOPATH
، حتى لو لم يتم تعيينها بشكل صريح:
go env GOPATH
استخدام
قم بإنشاء ملف hello.php
للاختبار:
<?php function f(...$xs) {} f(10); f(20); f(30); f($x); f();
قم بتشغيل phpgrep
عليه:
وجدنا جميع المكالمات إلى الدالة f
باستخدام وسيطة واحدة ، وهو رقم لا تساوي قيمته 20.
كيف يعمل phpgrep
لتحليل PHP ، يتم استخدام مكتبة github.com/z7zmey/php-parser . إنه جيد بما فيه الكفاية ، ولكن بعض قيود phpgrep
تتبع من ميزات المحلل اللغوي المستخدم. لا سيما الكثير من الصعوبات التي تنشأ عند محاولة العمل بشكل طبيعي مع الأقواس.
مبدأ phpgrep
بسيط:
- بنيت AST من قالب الإدخال ، يتم تفكيك المرشحات.
- لكل ملف إدخال ، يتم إنشاء شجرة AST كاملة ؛
- نلتف حول AST لكل ملف ، في محاولة للعثور على مثل هذه الأشجار الفرعية التي تطابق النمط ؛
- لكل نتيجة يتم تطبيق قائمة المرشحات.
- تتم طباعة جميع النتائج التي مرت المرشحات على الشاشة.
الأكثر إثارة للاهتمام هو كيف يتم مطابقة بالضبط العقدتين AST من أجل المساواة. تافه في بعض الأحيان: يمكن للواحد والواحد أن يلتقط أكثر من عنصر واحد. أمثلة العقد الفوقية هي ${"*"}
و ${"str"}
.
استنتاج
سيكون من غير phpgrep
التحدث عن phpgrep
دون ذكر البحث الهيكلي واستبدال (SSR) من PhpStorm. إنها تحل مشكلات مماثلة ، phpgrep
SSR مزاياها ، على سبيل المثال ، الاندماج في IDE ، ويفتخر phpgrep
بأنه برنامج مستقل ، وهو أسهل بكثير ، على سبيل المثال ، على CI.
من بين أشياء أخرى ، phpgrep
هي أيضًا مكتبة يمكنك استخدامها في برامجك لمطابقة كود PHP. هذا مفيد بشكل خاص لتوليد linter و code.
سأكون سعيدًا إذا كانت هذه الأداة مفيدة لك. إذا كانت هذه المقالة تحفزك فقط للبحث في اتجاه SSR المذكور أعلاه ، فهو جيد أيضًا.

مواد إضافية
يمكن العثور على القائمة الكاملة للأنماط التي تم استخدامها للتحليل في ملف patterns.txt . بجانب هذا الملف ، يمكنك العثور على البرنامج النصي phpgrep-lint.sh
، والذي يبسط عملية إطلاق phpgrep
مع قائمة القوالب.
لا تقدم المقالة قائمة كاملة بالردود ، ولكن يمكنك إعادة إنتاج التجربة عن طريق استنساخ جميع المستودعات المسماة وتشغيل phpgrep-lint.sh
عليها.
يمكنك استلهام قوالب الاختبار ، على سبيل المثال ، من مقالات استوديو PVS . أعجبتني التعبيرات المنطقية: الأخطاء التي ارتكبها المحترفون ، والتي تتحول إلى شيء مثل هذا:
# "x != y || x != z": phpgrep . '$x != $a || $x != $b' phpgrep . '$x !== $a || $x != $b' phpgrep . '$x != $a || $x !== $b' phpgrep . '$x !== $a || $x !== $b'
قد تكون مهتمًا أيضًا بالعرض التقديمي لـ phpgrep: البحث عن تعليمات برمجية مدمجة في بناء الجملة .
يستخدم المقال صوراً للغروفر التي تم إنشاؤها من خلال gopherkon .