PVS-Studio for Java تحت الغطاء: تطوير التشخيص



من أجل التغيير ، سنخبرك اليوم قليلاً عن عملية تطوير وإنهاء قواعد التشخيص لـ PVS-Studio Java. دعونا نرى لماذا لا تطفو مشغلات المحلل القديم كثيرًا من الإصدار إلى الإصدار ، وأن المشغلات الجديدة ليست مجنونة جدًا. وسنفسد أكثر بقليل "ما هي خطط javists" ونعرض اثنين من الأخطاء الجميلة (وليست كذلك) التي تم العثور عليها بمساعدة التشخيص من الإصدار التالي.



عمليات التشخيص وتطوير الاختبارات الذاتية



بطبيعة الحال ، تبدأ كل قاعدة تشخيصية جديدة بفكرة. ونظرًا لأن محلل Java هو أحدث اتجاه في تطوير PVS-Studio ، فإننا نسرق هذه الأفكار بشكل أساسي من أقسام C / C ++ و C #. ولكن ليس كل شيء سيئًا للغاية: فنحن نضيف أيضًا القواعد التي اخترعناها بأنفسنا (بما في ذلك من قبل المستخدمين - شكرًا!) ، بحيث تسرقها الأقسام نفسها منا لاحقًا. الدورة كما يقولون.



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



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



لذلك ، يجب العثور على الإيجابيات في المشاريع الحقيقية بطريقة ما أولاً. وتحتاج أيضًا إلى التحقق بطريقة ما مما يلي:



  • لن تسقط القاعدة على جنون المصادر المفتوحة ، حيث الحلول "الشيقة" شائعة ؛
  • ( - , );
  • data-flow ( ) - ;
  • open-source ;
  • over 9000%;
  • "" , ;
  • .


بشكل عام ، هنا ، مثل فارس على حصان (يعرج قليلاً ، لكننا نعمل على ذلك) ، يأتي SelfTester في المقدمة. مهمتها الرئيسية والوحيدة هي التحقق تلقائيًا من مجموعة من المشاريع وإظهار المشغلات التي تمت إضافتها أو اختفائها أو تغييرها بالنسبة إلى "المرجع" في نظام التحكم في الإصدار. قدم الاختلافات لتقرير المحلل وأظهر الكود المقابل في المشاريع ، باختصار. يقوم برنامج SelfTester for Java حاليًا باختبار 62 مشروعًا مفتوح المصدر لإصدارات ملتحية ، من بينها ، على سبيل المثال ، DBeaver و Hibernate و Spring. يستغرق التشغيل الكامل لجميع المشاريع من 2 إلى 2.5 ساعة ، وهو أمر مؤلم بلا شك ، لكن لا يمكن فعل أي شيء.





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



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



  • , . , " double-checked locking" ;
  • SelfTester-, ;
  • , -;
  • SelfTester- , ;
  • 3-4, ;
  • , , ( , );
  • , master.


لحسن الحظ ، تعد دورات SelfTester الكاملة نادرة بما يكفي ، ولا يتعين عليك الانتظار "2-2.5 ساعة" كثيرًا. من وقت لآخر ، تظهر تجاوزات الحظ والمحفزات في مشاريع كبيرة مثل Sakai و Apache Hive - حان الوقت لشرب القهوة وشرب القهوة وشرب القهوة. يمكنك أيضًا دراسة الوثائق ، لكن هذا ليس للجميع.



"لماذا نحتاج إلى اختبارات الوحدة ، حيث توجد هذه الأداة السحرية؟"



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



مشاكل جديدة في المعارف القدامى



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



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


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



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



اباتشي دوبو وقائمة فارغة



GitHub



Diagnostics " V6080 ضع في اعتبارك التحقق من الأخطاء المطبعية. من الممكن أن يتم التحقق من متغير معين في الحالة التالية " تم إصداره بالفعل في الإصدار 7.08 ، ولكنه لم يظهر في مقالاتنا بعد ، لذا حان الوقت لإصلاحه.



Menu.java:40



public class Menu
{
  private Map<String, List<String>> menus = new HashMap<String, List<String>>();

  public void putMenuItem(String menu, String item)
  {
    List<String> items = menus.get(menu);
    if (item == null)                      // <=
    {
      items = new ArrayList<String>();
      menus.put(menu, items);
    }
    items.add(item);
  }
  ....
}


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



public class Menu
{
  private Map<String, List<String>> menus = new HashMap<String, List<String>>();

  public void putMenuItem(String menu, String item)
  {
    List<String> items = menus.computeIfAbsent(menu, key -> new ArrayList<>());
    items.add(item);
  }
  ....
}


Glassfish وقفل مزدوج التحقق



GitHub



أحد التشخيصات التي سيتم تضمينها في الإصدار التالي هو التحقق من التنفيذ الصحيح لنمط "التحقق المزدوج من القفل". تبين أن Glassfish هو صاحب الرقم القياسي لعمليات الاكتشاف من مشروعات SelfTester: في المجموع ، وجد PVS-Studio 10 مناطق مشكلة في المشروع باستخدام هذه القاعدة. أدعو القارئ للحصول على بعض المرح والبحث عن اثنين منهم في مقتطف الشفرة أدناه. للحصول على المساعدة ، راجع الوثائق: " قفل التحقق المزدوج غير الآمن V6082 ". حسنًا ، أو ، إذا كنت لا تريد ذلك على الإطلاق ، في نهاية المقال.



EjbComponentAnnotationScanner.java



public class EjbComponentAnnotationScanner
{
  private Set<String> annotations = null;

  public boolean isAnnotation(String value)
  {
    if (annotations == null)
    {
      synchronized (EjbComponentAnnotationScanner.class)
      {
        if (annotations == null)
        {
          init();
        }
      }
    }
    return annotations.contains(value);
  }

  private void init()
  {
    annotations = new HashSet();
    annotations.add("Ljavax/ejb/Stateless;");
    annotations.add("Ljavax/ejb/Stateful;");
    annotations.add("Ljavax/ejb/MessageDriven;");
    annotations.add("Ljavax/ejb/Singleton;");
  }

  ....
}


SonarQube وتدفق البيانات







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



MetricRepositoryRule.java:90



protected void after()
{
  this.metricsById.clear();
  this.metricsById.clear();
}


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



protected void after()
{
  this.metricsById.clear();
  this.metricsById.clear();
}
public Metric getByKey(String key)
{
  Metric res = metricsByKey.get(key);
  ....
}


بالضبط. يحتوي الفصل على حقلين لهما نفس الأسماء metricsById و metricsByKey . أنا متأكد من أنه في بعد طريقة المطور أراد لمسح كل من القواميس، ولكن إما تكملة فشل له، أو دخل inertedly نفس الاسم. وبالتالي ، فإن القواميس التي تحتوي على بيانات ذات صلة ستكون غير متزامنة بعد الاستدعاء لـ after .



مجموعات ساكاي وفارغة



GitHub



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



DateModel.java:361



public List getDaySelectItems()
{
  List selectDays = new ArrayList();
  Integer[] d = this.getDays();
  for (int i = 0; i < d.length; i++)
  {
    SelectItem selectDay = new SelectItem(d[i], d[i].toString());
  }
  return selectDays;
}


بالمناسبة ، تحتوي نفس الفئة على طرق متشابهة جدًا بدون نفس الخطأ. على سبيل المثال:



public List getMonthSelectItems()
{
  List selectMonths = new ArrayList();
  Integer[] m = this.getMonths();
  for (int i = 0; i < m.length; i++)
  {
    SelectItem selectMonth = new SelectItem(m[i], m[i].toString());
    selectMonths.add(selectMonth);
  }
  return selectMonths;
}


خطط للمستقبل



بصرف النظر عن العديد من الأشياء الداخلية غير المثيرة للاهتمام ، فإننا نفكر في إضافة تشخيصات لـ Spring Framework إلى محلل Java. إنه ليس الخبز الرئيسي لجافيست فحسب ، بل يحتوي أيضًا على الكثير من اللحظات غير الواضحة التي يمكن للمرء أن يتعثر فيها. لسنا متأكدين تمامًا بعد من الشكل الذي ستظهر به هذه التشخيصات في النهاية ، ومتى سيحدث وما إذا كان سيحدث على الإطلاق. لكننا على يقين من أننا بحاجة إلى أفكار لهم ومشاريع مفتوحة المصدر باستخدام Spring for SelfTester. لذا ، إذا كان لديك شيء ما في الاعتبار ، فاقترحه (في التعليقات أو الرسائل الخاصة ، يمكنك أيضًا)! وكلما جمعنا المزيد من هذا الخير ، ستتحول الأولوية إليه.



وأخيرًا ، هناك أخطاء في تطبيق القفل الذي تم التحقق منه مرتين من Glassfish:



  • لم يتم الإعلان عن الحقل "غير مستقر".
  • يتم نشر الكائن أولاً ثم تهيئته.


لماذا كل هذا سيء - مرة أخرى ، يمكنك أن ترى في الوثائق .





إذا كنت ترغب في مشاركة هذه المقالة مع جمهور يتحدث الإنجليزية ، فيرجى استخدام رابط الترجمة: Nikita Lazeba. تحت غطاء PVS-Studio لجافا: كيف نطور التشخيص .



All Articles