محلل ثابت PVS-Studio معروف في عالم C و C ++ و C # كأداة للكشف عن الأخطاء ونقاط الضعف المحتملة. ومع ذلك ، لدينا عدد قليل من العملاء من القطاع المالي ، حيث اتضح أن Java و IBM RPG (!) مطلوبة الآن هناك. لكننا أردنا دائمًا الاقتراب من عالم Enterprise ، لذلك ، بعد بعض التفكير ، قررنا البدء في إنشاء محلل Java.
مقدمة
بالطبع ، كانت هناك مخاوف. من السهل أن تأخذ سوق المحلل في IBM RPG. لست متأكدًا على الإطلاق من وجود أدوات لائقة للتحليل الثابت لهذه اللغة. في عالم جافا ، تختلف الأمور تمامًا. هناك بالفعل مجموعة من الأدوات للتحليل الثابت ، وللمضي قدمًا ، تحتاج إلى إنشاء محلل قوي ورائع حقًا.
ومع ذلك ، فإن شركتنا لديها خبرة في استخدام العديد من الأدوات للتحليل الثابت لجافا ، ونحن على يقين من أنه يمكننا القيام بأشياء كثيرة بشكل أفضل.
بالإضافة إلى ذلك ، كانت لدينا فكرة عن كيفية استخدام القوة الكاملة لمحلل C ++ الخاص بنا في محلل Java. لكن أول الأشياء أولاً.
الشجرة
بادئ ذي بدء ، كان من الضروري تحديد كيف نحصل على شجرة النحو والنموذج الدلالي.
شجرة بناء الجملة هي العنصر الأساسي الذي يتم بناء المحلل حوله. عند إجراء عمليات الفحص ، ينتقل المحلل من خلال شجرة بناء الجملة ويفحص العقد الفردية الخاصة به. بدون هذه الشجرة ، من المستحيل عمليا إجراء تحليل ثابت جاد. على سبيل المثال ، البحث عن الأخطاء باستخدام التعبيرات العادية أمر
غير واعد .
وتجدر الإشارة إلى أن شجرة بناء الجملة ليست كافية. يحتاج المحلل أيضًا إلى معلومات دلالية. على سبيل المثال ، نحتاج إلى معرفة أنواع جميع عناصر الشجرة ، وأن نتمكن من الانتقال إلى تعريف المتغير ، وما إلى ذلك.
لقد درسنا العديد من الخيارات للحصول على شجرة النحو والنموذج الدلالي:
لقد تخلينا عن فكرة استخدام ANTLR على الفور تقريبًا ، لأن هذا سيعقد تطوير المحلل بدون داعٍ (يجب تنفيذ التحليل الدلالي بمفردنا). في النهاية ، قررنا التوقف عند مكتبة Spoon:
- إنه ليس مجرد محلل ، ولكن نظامًا بيئيًا بالكامل - فهو لا يوفر فقط شجرة التحليل ، بل يوفر أيضًا فرصًا للتحليل الدلالي ، على سبيل المثال ، يتيح لك الحصول على معلومات حول أنواع المتغيرات ، والانتقال إلى تعريف المتغير ، والحصول على معلومات حول الفصل الأصل ، وما إلى ذلك.
- وهو يعتمد على Eclipse JDT وقادر على تجميع الكود.
- وهو يدعم أحدث إصدار من Java ويتم تحديثه باستمرار.
- وثائق جيدة وواجهة برمجة تطبيقات واضحة.
في ما يلي مثال على نموذج metamodel الذي يوفره Spoon والذي نعمل عليه عند إنشاء قواعد التشخيص:
هذا النموذج مطابق للرمز التالي:
class TestClass { void test(int a, int b) { int x = (a + b) * 4; System.out.println(x); } }
أحد الأشياء اللطيفة في Spoon هو أنه يبسط شجرة البنية (إزالة العقد وإضافتها) لتسهيل العمل معها. وهذا يضمن التكافؤ الدلالي للنموذج الأصلي المبسط للأصل.
بالنسبة لنا ، هذا يعني ، على سبيل المثال ، أننا لم نعد بحاجة للقلق بشأن تخطي الأقواس الإضافية عند عبور شجرة. بالإضافة إلى ذلك ، يتم وضع كل تعبير في كتلة ، ويتم الكشف عن عمليات الاستيراد ، ويتم إجراء بعض التبسيطات المماثلة الأخرى.
على سبيل المثال ، رمز مثل هذا:
for (int i = ((0)); (i < 10); i++) if (cond) return (((42)));
ستعرض على النحو التالي:
for (int i = 0; i < 10; i++) { if (cond) { return 42; } }
بناء على شجرة النحو ، يتم إجراء ما يسمى التحليل القائم على النمط. هذا هو البحث عن أخطاء في التعليمات البرمجية المصدر لبرنامج باستخدام أنماط رمز الخطأ المعروفة. في أبسط الحالات ، يبحث المحلل عن الأماكن التي تبدو وكأنها خطأ في الشجرة وفقًا للقواعد الموضحة في التشخيصات المقابلة. عدد هذه الأنماط كبير ويمكن أن يختلف تعقيدها بشكل كبير.
أبسط مثال على خطأ تم اكتشافه بواسطة التحليل القائم على النمط هو الكود التالي من مشروع jMonkeyEngine:
if (p.isConnected()) { log.log(Level.FINE, "Connection closed:{0}.", p); } else { log.log(Level.FINE, "Connection closed:{0}.", p); }
الكتل
ثم وغيرها من
العبارة if هي نفسها ؛ على الأرجح ، هناك خطأ منطقي.
هنا مثال آخر مشابه من مشروع Hive:
if (obj instanceof Number) {
يحتوي هذا الرمز على شرطين متطابقين في تسلسل من النموذج
إذا (...) آخر إذا (....) آخر إذا (....) . يجدر التحقق من هذا القسم من التعليمات البرمجية بحثًا عن خطأ منطقي ، أو إزالة الرمز المكرر.
تحليل تدفق البيانات
بالإضافة إلى شجرة النحو والنموذج الدلالي ، يحتاج المحلل إلى آلية
لتحليل تدفق البيانات .
يسمح لك تحليل تدفق البيانات بحساب القيم الصالحة للمتغيرات والتعبيرات عند كل نقطة في البرنامج ، وبفضل هذا ، ابحث عن الأخطاء. نسمي هذه القيم الصالحة القيم الافتراضية.
يتم إنشاء القيم الافتراضية للمتغيرات وحقول الفئة ومعلمات الطريقة وأشياء أخرى عند الإشارة الأولى. إذا كان هذا التعيين ، فإن آلية تدفق البيانات تحسب القيمة الافتراضية عن طريق تحليل التعبير الموجود على اليمين ، وإلا فسيتم اعتبار نطاق القيم الصالحة لهذا النوع من المتغيرات كقيمة افتراضية. على سبيل المثال:
void func(byte x)
في كل مرة تتغير فيها قيمة متغير ، يعيد محرك تدفق البيانات حساب القيمة الافتراضية. على سبيل المثال:
void func() { int x = 5;
يقوم محرك تدفق البيانات أيضًا بمعالجة عبارات التحكم:
void func(int x)
في هذا المثال ، عند إدخال الدالة ، لا توجد معلومات حول نطاق قيم المتغير
x ، وبالتالي يتم تعيينها وفقًا لنوع المتغير (من -2147483648 إلى 2147483647). ثم تفرض الكتلة الشرطية الأولى قيدًا
x > 3 ، ويتم دمج النطاقات. نتيجة لذلك ، في كتلة
ثم نطاق القيم لـ
x من 4 إلى 2147483647 ، وفي الكتلة
الأخرى من -2147483648 إلى 3. تتم معالجة الشرط الثاني
x <10 بنفس الطريقة.
بالإضافة إلى ذلك ، يجب أن تكون قادرًا على إجراء حسابات رمزية بحتة. المثال الأبسط:
void f1(int a, int b, int c) { a = c; b = c; if (a == b)
هنا ،
يتم تعيين القيمة
c للمتغير
a ، ويتم أيضًا تعيين المتغير
b للقيمة
c ، وبعد
ذلك تتم مقارنة
a و
b . في هذه الحالة ، للعثور على الخطأ ، فقط تذكر قطعة الخشب المقابلة للجانب الأيمن.
فيما يلي مثال أكثر تعقيدًا بعض الشيء مع حسابات الشخصيات:
void f2(int a, int b, int c) { if (a < b) { if (b < c) { if (c < a)
في مثل هذه الحالات ، من الضروري بالفعل حل نظام عدم المساواة في شكل رمزي.
تساعد آلية تدفق البيانات المحلل في العثور على الأخطاء التي يصعب العثور عليها باستخدام التحليل المعتمد على الأنماط.
تتضمن هذه الأخطاء:
- يفيض
- السفر إلى الخارج ؛
- الوصول عن طريق إشارة فارغة أو يحتمل أن تكون فارغة ؛
- شروط لا معنى لها (دائمًا صحيحة / خاطئة) ؛
- تسرب الذاكرة والموارد ؛
- القسمة على 0 ؛
- والبعض الآخر.
تحليل تدفق البيانات مهم بشكل خاص عند البحث عن نقاط الضعف. على سبيل المثال ، إذا تلقى برنامج معين مدخلات من المستخدم ، فمن المحتمل أن يتم استخدام الإدخال للتسبب في رفض الخدمة ، أو للسيطرة على النظام. تتضمن الأمثلة الأخطاء التي تسبب تجاوز سعة المخزن المؤقت لبعض المدخلات ، أو ، على سبيل المثال ، حقن SQL. في كلتا الحالتين ، لكي يكتشف المحلل الثابت مثل هذه الأخطاء ونقاط الضعف ، من الضروري مراقبة تدفق البيانات والقيم المحتملة للمتغيرات.
يجب أن أقول أن آلية تحليل تدفق البيانات هي آلية معقدة وشاملة ، وفي هذه المقالة تناولت الأساسيات فقط.
دعنا نلقي نظرة على بعض الأمثلة على الأخطاء التي يمكن اكتشافها باستخدام آلية تدفق البيانات.
مشروع الخلية:
public static boolean equal(byte[] arg1, final int start1, final int len1, byte[] arg2, final int start2, final int len2) { if (len1 != len2) {
الشرط
len1 == len2 مستوفى دائمًا ، نظرًا لأن الفحص المعاكس قد تم إجراؤه بالفعل أعلاه.
مثال آخر من نفس المشروع:
if (instances != null) {
هنا ، في الكتلة
الأخرى ، يتم ضمان إلغاء الإشارة إلى المؤشر الفارغ. ملحوظة:
الحالات هنا هي نفسها.
مثال من مشروع JMonkeyEngine:
public static int convertNewtKey(short key) { .... if (key >= 0x10000) { return key - 0x10000; } return 0; }
هنا يتم مقارنة المتغير
الرئيسي بالرقم 65536 ، ومع ذلك ، فهو من النوع
القصير ، والحد الأقصى للقيمة الممكنة للقصر هو 32767.
مثال من مشروع جنكينز:
public final R getSomeBuildWithWorkspace() { int cnt = 0; for (R b = getLastBuild(); cnt < 5 && b ! = null; b = b.getPreviousBuild()) { FilePath ws = b.getWorkspace(); if (ws != null) return b; } return null; }
تم إدخال المتغير
cnt في هذا الرمز للحد من عدد التمريرات إلى خمسة ، ولكن نسيت زيادته ، ونتيجة لذلك يكون الاختيار عديم الفائدة.
آلية الشرح
بالإضافة إلى ذلك ، يحتاج المحلل إلى آلية التعليق التوضيحي. التعليقات التوضيحية هي نظام ترميز يزود المحلل بمعلومات إضافية حول الأساليب والفئات المستخدمة ، بالإضافة إلى ما يمكن الحصول عليه من خلال تحليل توقيعه. يتم الترميز يدويًا ، إنها عملية طويلة وشاقة ، نظرًا لتحقيق أفضل النتائج ، من الضروري إضافة تعليق على عدد كبير من الفئات والأساليب القياسية للغة جافا. من المنطقي أيضًا إضافة تعليق إلى المكتبات الشائعة. بشكل عام ، يمكن اعتبار التعليقات التوضيحية بمثابة قاعدة معرفية للمحلل حول عقود الطرق والفئات القياسية.
في ما يلي مثال صغير على خطأ يمكن اكتشافه باستخدام التعليقات التوضيحية:
int test(int a, int b) { ... return Math.max(a, a); }
في هذا المثال ، بسبب خطأ مطبعي ، تم تمرير نفس المتغير كالوسيطة الثانية إلى أسلوب
Math.max كالوسيطة الأولى. مثل هذا التعبير لا معنى له ومريب.
مع العلم أن حجج طريقة
Math.max يجب أن تكون مختلفة دائمًا ، سيكون المحلل الثابت قادرًا على إصدار تحذير لمثل هذا الرمز.
في المستقبل ، سأقدم بعض الأمثلة على ترميزنا للفصول والأساليب المضمنة (رمز C ++):
Class("java.lang.Math") - Function("abs", Type::Int32) .Pure() .Set(FunctionClassification::NoDiscard) .Returns(Arg1, [](const Int &v) { return v.Abs(); }) - Function("max", Type::Int32, Type::Int32) .Pure() .Set(FunctionClassification::NoDiscard) .Requires(NotEquals(Arg1, Arg2) .Returns(Arg1, Arg2, [](const Int &v1, const Int &v2) { return v1.Max(v2); }) Class("java.lang.String", TypeClassification::String) - Function("split", Type::Pointer) .Pure() .Set(FunctionClassification::NoDiscard) .Requires(NotNull(Arg1)) .Returns(Ptr(NotNullPointer)) Class("java.lang.Object") - Function("equals", Type::Pointer) .Pure() .Set(FunctionClassification::NoDiscard) .Requires(NotEquals(This, Arg1)) Class("java.lang.System") - Function("exit", Type::Int32) .Set(FunctionClassification::NoReturn)
التفسيرات:
- فئة - فئة مشروحة
- الوظيفة - طريقة الطبقة المشروحة ؛
- نقي - تعليق توضيحي يوضح أن الطريقة نظيفة ، أي حتمية وبدون آثار جانبية ؛
- Set - تعيين علامة عشوائية للطريقة.
- FunctionClassification :: NoDiscard - علامة تعني أنه يجب استخدام القيمة المرجعة للطريقة ؛
- FunctionClassification :: NoReturn - علامة تشير إلى أن الطريقة لا تُرجع التحكم ؛
- Arg1 ، Arg2 ، ... ، ArgN - وسيطات الأسلوب ؛
- المرتجعات - القيمة المرتجعة للطريقة ؛
- يتطلب - عقد للطريقة.
تجدر الإشارة إلى أنه بالإضافة إلى الترميز اليدوي ، هناك نهج آخر للتعليق - الإخراج التلقائي للعقود استنادًا إلى رمز البايت. من الواضح أن هذا النهج يسمح لك بعرض أنواع معينة فقط من العقود ، ولكنه يجعل من الممكن الحصول على معلومات إضافية بشكل عام من جميع التبعيات ، وليس فقط من تلك التي تم التعليق عليها يدويًا.
بالمناسبة ، توجد بالفعل أداة يمكنها عرض العقود مثل
Nullable و
NotNull استنادًا إلى bytecode -
FABA . كما أفهمها ، يتم استخدام مشتق FABA في IntelliJ IDEA.
الآن نحن نفكر أيضًا في إضافة تحليل البايت كود للحصول على عقود لجميع الطرق ، حيث يمكن لهذه العقود أن تكمل التعليقات التوضيحية اليدوية.
غالبًا ما تشير قواعد التشخيص في العمل إلى التعليقات التوضيحية. بالإضافة إلى التشخيص ، تستخدم التعليقات التوضيحية آلية تدفق البيانات. على سبيل المثال ، باستخدام التعليق التوضيحي لأسلوب
java.lang.Math.abs ، يمكنه حساب قيمة وحدة الأعداد بدقة. في الوقت نفسه ، لا يتعين عليك كتابة أي رمز إضافي - فقط ضع علامة على الطريقة بشكل صحيح.
ضع في اعتبارك مثالاً على خطأ من مشروع الإسبات يمكن اكتشافه من خلال التعليق التوضيحي:
public boolean equals(Object other) { if (other instanceof Id) { Id that = (Id) other; return purchaseSequence.equals(this.purchaseSequence) && that.purchaseNumber == this.purchaseNumber; } else { return false; } }
يقارن أسلوب
يساوي () في هذا الرمز ، الكائن PurchaseSequence مع نفسه. من المؤكد أن هذا خطأ مطبعي وعلى اليمين يجب أن يكون
ذلك .
شراء متساوي ، وليس
شراء متساوي .
كيف قام دكتور فرانكنشتاين بتجميع محلل من الأجزاء
نظرًا لأن آليات تدفق البيانات والتعليقات التوضيحية نفسها لا ترتبط ارتباطًا وثيقًا بلغة معينة ، فقد تقرر إعادة استخدام هذه الآليات من محلل C ++ الخاص بنا. هذا سمح لنا بالحصول بسرعة على كل قوة جوهر محلل C ++ في محلل Java الخاص بنا. بالإضافة إلى ذلك ، تأثر هذا القرار أيضًا بحقيقة أن هذه الآليات كانت مكتوبة بلغة C ++ الحديثة مع مجموعة من الرسوم البيانية وسحر القوالب ، وبالتالي ، ليست مناسبة جدًا للانتقال إلى لغة أخرى.
من أجل ربط جزء Java مع نواة C ++ ، قررنا استخدام
SWIG (برنامج Simplified Wrapper and Interface Generator) - وهي أداة لتوليد أغلفة وواجهات تلقائيًا لربط برامج C و C ++ ببرامج مكتوبة بلغات أخرى. بالنسبة إلى Java ، تقوم SWIG بإنشاء رمز
JNI (واجهة Java الأصلية) .
يعتبر SWIG رائعًا للحالات التي يوجد بها بالفعل كمية كبيرة من كود C ++ التي يجب دمجها في مشروع Java.
سأعطيك الحد الأدنى من العمل مع SWIG. افترض أن لدينا فئة C ++ التي نريد استخدامها في مشروع Java:
CoolClass.h
class CoolClass { public: int val; CoolClass(int val); void printMe(); };
CoolClass.cpp
#include <iostream> #include "CoolClass.h" CoolClass::CoolClass(int v) : val(v) {} void CoolClass::printMe() { std::cout << "val: " << val << '\n'; }
تحتاج أولاً إلى إنشاء
ملف واجهة SWIG مع وصف لجميع الوظائف والفئات المصدرة. أيضًا في هذا الملف ، إذا لزم الأمر ، يتم إجراء إعدادات إضافية.
مثال i
%module MyModule %{ #include "CoolClass.h" %} %include "CoolClass.h"
بعد ذلك ، يمكنك تشغيل SWIG:
$ swig -c++ -java Example.i
سيتم إنشاء الملفات التالية:
- CoolClass.java - فصل سنعمل معه مباشرة في مشروع Java ؛
- MyModule.java - فئة نموذجية يتم فيها وضع جميع الوظائف والمتغيرات المجانية ؛
- MyModuleJNI.java - أغلفة جافا ؛
- Example_wrap.cxx - أغلفة C ++.
تحتاج الآن فقط إلى إضافة ملفات .java الناتجة إلى مشروع Java وملف .cxx إلى مشروع C ++.
أخيرًا ، تحتاج إلى تجميع مشروع C ++ كمكتبة ديناميكية وتحميله في مشروع Java باستخدام
System.loadLibary () :
App.java
class App { static { System.loadLibary("example"); } public static void main(String[] args) { CoolClass obj = new CoolClass(42); obj.printMe(); } }
من الناحية التخطيطية ، يمكن تمثيل ذلك على النحو التالي:
بالطبع ، في مشروع حقيقي ، كل شيء ليس بهذه البساطة وعليك بذل المزيد من الجهد:
- من أجل استخدام فئات وأساليب القالب من C ++ ، يجب أن يتم نسخها لجميع معلمات القالب المقبولة باستخدام توجيه القالب٪ ؛
- في بعض الحالات ، قد تحتاج إلى التقاط الاستثناءات التي تم طرحها من جزء C ++ في جزء Java. بشكل افتراضي ، لا تقوم SWIG بالتقاط الاستثناءات من C ++ (يحدث خطأ) ، ومع ذلك ، من الممكن القيام بذلك باستخدام توجيه الاستثناء٪ ؛
- يسمح لك SWIG بتمديد رمز الموقع المفتوح على جانب Java باستخدام التوجيه ٪ extension. على سبيل المثال ، في مشروعنا ، نضيف طريقة toString () إلى القيم الافتراضية حتى نتمكن من عرضها في مصحح أخطاء Java ؛
- من أجل محاكاة سلوك RAII من C ++ ، يتم تنفيذ واجهة AutoClosable في جميع فئات الاهتمام ؛
- تتيح آلية المخرجين استخدام تعدد الأشكال عبر اللغات ؛
- بالنسبة للأنواع التي تم تخصيصها فقط داخل C ++ (في مجموعة الذاكرة الخاصة بهم) ، تتم إزالة المُنشئات والمنهيات لتحسين الأداء. سيتجاهل جامع القمامة هذه الأنواع.
يمكنك قراءة المزيد عن كل هذه الآليات في
وثائق SWIG .
تم تصميم محللنا باستخدام gradle ، الذي يستدعي CMake ، والذي بدوره يستدعي SWIG ويجمع جزء C ++. بالنسبة للمبرمجين ، يحدث هذا بشكل غير محسوس تقريبًا ، لذلك لا نشعر بأي إزعاج خاص أثناء التطوير.
تم تصميم جوهر محلل C ++ الخاص بنا تحت أنظمة Windows و Linux و macOS ، لذا يعمل محلل Java أيضًا في أنظمة التشغيل هذه.
ما هي قاعدة التشخيص؟
التشخيصات نفسها ورمز التحليل مكتوب بلغة جافا. هذا بسبب التفاعل الوثيق مع ملعقة. كل قاعدة تشخيصية هي زائر ، يتم تحميل طرقه بشكل زائد ، حيث يتم التحايل على العناصر التي تهمنا:
على سبيل المثال ، يبدو إطار عمل التشخيص V6004 كما يلي:
class V6004 extends PvsStudioRule { .... @Override public void visitCtIf(CtIf ifElement) {
الإضافات
من أجل دمج أسهل للمحلل الساكن في المشروع ، قمنا بتطوير مكونات إضافية لأنظمة التجميع Maven و Gradle. يمكن للمستخدم إضافة المكون الإضافي فقط إلى المشروع.
بالنسبة لـ Gradle:
.... apply plugin: com.pvsstudio.PvsStudioGradlePlugin pvsstudio { outputFile = 'path/to/output.json' .... }
بالنسبة لـ Maven:
.... <plugin> <groupId>com.pvsstudio</groupId> <artifactId>pvsstudio-maven-plugin</artifactId> <version>0.1</version> <configuration> <analyzer> <outputFile>path/to/output.json</outputFile> .... </analyzer> </configuration> </plugin>
بعد ذلك ، سيتلقى المكون الإضافي بشكل مستقل هيكل المشروع ويبدأ التحليل.
بالإضافة إلى ذلك ، قمنا بتطوير البرنامج المساعد لنموذج IntelliJ IDEA.
يعمل هذا البرنامج المساعد أيضًا في Android Studio.
هناك مكون إضافي لـ Eclipse قيد التطوير حاليًا.
تحليل تدريجي
لقد قدمنا وضع تحليل تزايدي يسمح لك بفحص الملفات المعدلة فقط وبالتالي يقلل بشكل كبير من الوقت المطلوب لتحليل الكود. وبفضل هذا ، سيتمكن المطورون من تشغيل التحليل كلما كان ذلك ضروريًا.
يتضمن التحليل التدريجي عدة مراحل:
- التخزين المؤقت ملعقة metamodel.
- إعادة بناء الجزء المتغير من metamodel ؛
- تحليل الملفات المتغيرة.
نظام الاختبار لدينا
لاختبار محلل جافا على المشاريع الحقيقية ، قمنا بكتابة مجموعة أدوات خاصة تسمح لك بالعمل مع قاعدة بيانات المشاريع المفتوحة. تم كتابته في ^ W Python + Tkinter وهو متعدد المنصات.
يعمل على النحو التالي:
- يتم تنزيل المشروع التجريبي لنسخة معينة من المستودع على GitHub ؛
- المشروع قيد التجميع ؛
- تمت إضافة المكون الإضافي إلى pom.xml أو build.gradle (باستخدام git Apply ) ؛
- يتم تشغيل المحلل الثابت باستخدام البرنامج المساعد ؛
- يتم مقارنة التقرير الناتج مع المعيار لهذا المشروع.
يضمن هذا النهج عدم فقدان الاستجابات الجيدة نتيجة لتغيير كود المحلل. فيما يلي واجهة أداة الاختبار الخاصة بنا.
يتم تمييز تلك المشاريع في التقارير التي لديها أي اختلافات مع المعيار باللون الأحمر. يتيح لك زر الموافقة حفظ النسخة الحالية من التقرير كمرجع.
أمثلة على الخطأ
بالتقليد ، سأذكر العديد من الأخطاء من العديد من المشاريع المفتوحة التي وجدها محلل جافا. في المستقبل ، من المقرر كتابة مقالات بتقرير أكثر تفصيلاً عن كل مشروع.
مشروع السبات
تحذير PVS-Studio: وظيفة "يساوي" V6009 تتلقى الحجج الفردية. فحص الحجج: هذا ، 1. PurchaseRecord.java 57
public boolean equals(Object other) { if (other instanceof Id) { Id that = (Id) other; return purchaseSequence.equals(this.purchaseSequence) && that.purchaseNumber == this.purchaseNumber; } else { return false; } }
يقارن أسلوب
يساوي () في هذا الرمز ، الكائن PurchaseSequence مع نفسه. على الأرجح ، هذا خطأ مطبعي وعلى اليمين يجب أن يكون
ذلك .
شراء مساواة ، وليس
شراء مساواة .
تحذير PVS-Studio: وظيفة "يساوي" V6009 تتلقى الحجج الفردية. فحص الحجج: هذا ، 1. ListHashcodeChangeTest.java 232
public void removeBook(String title) { for( Iterator<Book> it = books.iterator(); it.hasNext(); ) { Book book = it.next(); if ( title.equals( title ) ) { it.remove(); } } }
يجب أن تكون
عملية مشابهة لتلك السابقة - على اليمين
عنوان الكتاب وليس
العنوان .
خلية المشروع
تحذير
PVS-Studio: التعبير V6007 'colOrScalar1.equals ("العمود") دائمًا ما يكون خطأ. GenVectorCode.java 2768
تحذير
PVS-Studio: التعبير V6007 'colOrScalar1.equals ("Scalar") دائمًا ما يكون خطأ. جينفيكتور كود. java 2774
تحذير
PVS-Studio: التعبير V6007 'colOrScalar1.equals ("العمود") دائمًا ما يكون خطأ. GenVectorCode.java 2785
String colOrScalar1 = tdesc[4]; .... if (colOrScalar1.equals("Col") && colOrScalar1.equals("Column")) { .... } else if (colOrScalar1.equals("Col") && colOrScalar1.equals("Scalar")) { .... } else if (colOrScalar1.equals("Scalar") && colOrScalar1.equals("Column")) { .... }
يتم الخلط بين عوامل التشغيل هنا وبدلاً من '
||' تستخدم "
&&" .
مشروع JavaParser
تحذير
PVS-Studio: V6001 هناك تعبيرات فرعية متطابقة "tokenRange.getBegin (). GetRange (). IsPresent ()" إلى اليسار وإلى يمين عامل التشغيل "&&". Node.java 213
public Node setTokenRange(TokenRange tokenRange) { this.tokenRange = tokenRange; if (tokenRange == null || !(tokenRange.getBegin().getRange().isPresent() && tokenRange.getBegin().getRange().isPresent())) { range = null; } else { range = new Range( tokenRange.getBegin().getRange().get().begin, tokenRange.getEnd().getRange().get().end); } return this; }
وجد المحلل أن نفس التعبيرات موجودة على يسار ويمين عامل التشغيل
&& (في حين أن جميع الطرق في سلسلة المكالمات نظيفة). على الأرجح ، في الحالة الثانية ، من الضروري استخدام
tokenRange.getEnd () ، وليس
tokenRange.getBegin () .
تحذير PVS-Studio: V6016 وصول مشبوه إلى عنصر كائن 'typeDeclaration.getTypeParameters ()' بواسطة فهرس ثابت داخل حلقة. ResolveReferenceType.java 265 if (!isRawType()) { for (int i=0; i<typeDeclaration.getTypeParams().size(); i++) { typeParametersMap.add( new Pair<>(typeDeclaration.getTypeParams().get(0), typeParametersValues().get(i))); } }
اكتشف المحلل وصولاً مشبوهًا إلى عنصر المجموعة في فهرس ثابت داخل الحلقة. قد يكون هناك خطأ في هذا الرمز.مشروع جنكينز
تحذير PVS-Studio: التعبير V6007 'cnt <5' صحيح دائمًا. الحلقة 557 public final R getSomeBuildWithWorkspace() { int cnt = 0; for (R b = getLastBuild(); cnt < 5 && b ! = null; b = b.getPreviousBuild()) { FilePath ws = b.getWorkspace(); if (ws != null) return b; } return null; }
تم إدخال المتغير cnt في هذا الرمز للحد من عدد التمريرات إلى خمسة ، ولكن نسيت زيادته ، ونتيجة لذلك يكون الاختيار عديم الفائدة.مشروع سبارك
تحذير استوديو من PVS: '! sparkApplications = فارغة' V6007 التعبير هو دائما إلى true. SparkFilter.java 127 if (StringUtils.isNotBlank(applications)) { final String[] sparkApplications = applications.split(","); if (sparkApplications != null && sparkApplications.length > 0) { ... } }
إن التحقق من قيمة النتيجة التي تم إرجاعها بواسطة طريقة الانقسام لا معنى له ، لأن هذه الطريقة دائمًا ما ترجع مجموعة ولا تُرجع قيمة فارغة .مشروع الملعقة
تحذير PVS-Studio: V6001 هناك تعبيرات فرعية متطابقة '! M.getSimpleName (). يبدأ مع ("مجموعة") إلى اليسار وإلى يمين عامل التشغيل "&&". الحلقة 108 if (!m.getSimpleName().startsWith("set") && !m.getSimpleName().startsWith("set")) { continue; }
في هذا الكود ، تكون نفس التعبيرات على يسار ويمين عامل التشغيل && (جميع الطرق في سلسلة المكالمات نظيفة). على الأرجح ، يحتوي الرمز على خطأ منطقي. تحذيرPVS-Studio: التعبير V6007 'idxOfScopeBoundTypeParam> = 0' صحيح دائمًا. الطريقة المثالية private boolean isSameMethodFormalTypeParameter(....) { .... int idxOfScopeBoundTypeParam = getIndexOfTypeParam(....); if (idxOfScopeBoundTypeParam >= 0) {
هنا تم إغلاقها في الشرط الثاني وبدلاً من idxOfSuperBoundTypeParam كتب idxOfScopeBoundTypeParam .مشروع أمن الربيع
تحذير PVS-Studio: V6001 هناك تعبيرات فرعية متطابقة إلى اليسار وإلى اليمين من "||" عامل. خطوط التحقق: 38 ، 39. AnyRequestMatcher.java 38 @Override @SuppressWarnings("deprecation") public boolean equals(Object obj) { return obj instanceof AnyRequestMatcher || obj instanceof security.web.util.matcher.AnyRequestMatcher; }
تشبه العملية العملية السابقة - هنا تتم كتابة اسم نفس الفصل بطرق مختلفة.تحذير PVS-Studio: V6006 تم إنشاء الكائن ولكن لم يتم استخدامه. قد تكون الكلمة الرئيسية "قذف" مفقودة. 434 if (!expectedNonceSignature.equals(nonceTokens[1])) { new BadCredentialsException( DigestAuthenticationFilter.this.messages .getMessage("DigestAuthenticationFilter.nonceCompromised", new Object[] { nonceAsPlainText }, "Nonce token compromised {0}")); }
في هذا الرمز ، نسوا إضافة رمي قبل الاستثناء. ونتيجة لذلك ، يتم طرح كائن استثناء BadCredentialsException ، ولكن لا يتم استخدامه بأي شكل من الأشكال ، أي لا يتم طرح استثناء.تحذير PVS-Studio: V6030 الطريقة الموجودة على يمين "|" سيتم استدعاء عوامل التشغيل بغض النظر عن قيمة المعامل الأيسر. ربما ، من الأفضل استخدام '||'. RedirectUrlBuilder.java 38 public void setScheme(String scheme) { if (!("http".equals(scheme) | "https".equals(scheme))) { throw new IllegalArgumentException("..."); } this.scheme = scheme; }
في هذا الكود استخدام | غير مبرر ، لأنه عند استخدامه ، سيتم حساب الجانب الأيمن حتى إذا كان الجانب الأيسر صحيحًا بالفعل. في هذه الحالة ، هذا ليس له معنى عملي ، وبالتالي العامل | يستحق الاستبدال بـ || .
مشروع IntelliJ IDEA
تحذير PVS-Studio: V6008 احتمال عدم الإشارة إلى "المحرر". IntroducVariableBase.java: 609 final PsiElement nameSuggestionContext = editor == null ? null : file.findElementAt(...);
اكتشف المحلل أنه في هذا الكود قد يحدث انحراف في المؤشر الحر للمحرر . يجدر إضافة شيك إضافي.تحذير PVS-Studio: تعبير V6007 غير صحيح دائمًا. RefResolveServiceImpl.java: 814 @Override public boolean contains(@NotNull VirtualFile file) { .... return false & !myProjectFileIndex.isUnderSourceRootOfType(....); }
من الصعب بالنسبة لي أن أقول ما كان يفكر فيه المؤلف ، لكن مثل هذا الرمز يبدو مريبًا للغاية. حتى لو لم يكن هناك فجأة خطأ هنا ، أعتقد أن الأمر يستحق إعادة كتابة هذا المكان حتى لا تربك المحلل والمبرمجين الآخرين.تحذير استوديو من PVS: V6007 التعبير "من نتيجة [0] 'غير صحيح دائما. كوبيكليس هاندلر جافا: 298 final boolean[] result = new boolean[] {false};
أظن أنهم هنا نسوا تغيير القيمة في النتيجة بطريقة أو بأخرى . ولهذا السبب ، أفاد المحلل أن التحقق مما إذا كانت (النتيجة [0]) لا طائل من ورائها.الخلاصة
اتجاه جافا متعدد الاستخدامات للغاية - إنه سطح مكتب ، و android ، وويب ، وأكثر من ذلك بكثير ، لذلك لدينا مساحة كبيرة للنشاط. أولاً وقبل كل شيء ، بالطبع ، سنطور تلك المناطق التي ستكون مطلوبة أكثر.فيما يلي خططنا للمستقبل القريب:- شروح الإخراج على أساس بايت كود ؛
- الاندماج في مشاريع Ant (يستخدمها شخص آخر في 2018؟) ؛
- البرنامج المساعد لكسوف (قيد التطوير) ؛
- المزيد من التشخيص والشروح ؛
- تحسين آلية تدفق البيانات.
أقترح أيضًا أولئك الذين يرغبون في المشاركة في اختبار إصدار ألفا من محلل جافا عندما يصبح متاحًا. للقيام بذلك، اكتب لنا في الدعم . سنضيف جهة الاتصال الخاصة بك إلى القائمة ونكتب إليك عندما نقوم بإعداد أول نسخة ألفا.
إذا كنت ترغب في مشاركة هذه المقالة مع جمهور يتحدث الإنجليزية ، فيرجى استخدام رابط الترجمة: Egor Bredikhin. تطوير محلل ثابت جديد: