التحقق من مشروع CDK باستخدام محلل ثابت IntelliJ IDEA

قررت اختبار محلل كود Java الثابت من IntelliJ IDEA ، واختبرت معه مشروع The Chemistry Development Kit . هنا سأقدم بعض الأخطاء التي وجدتها. أعتقد أن بعضها نموذجي لبرامج Java ككل ، حتى تكون ممتعة.


The Chemistry Development Kit هي مكتبة جافا مفتوحة المصدر لحل مشاكل المعلوماتية الكيميائية والمعلوماتية الحيوية. عندما كنت منشغلة في المعلوماتية الحيوية ، استخدمناها بنشاط. المشروع قيد التطوير منذ أكثر من 20 عامًا ، ويضم العشرات من المؤلفين ، كما أن جودة الكود هناك غير متساوية جدًا. ومع ذلك ، هناك اختبارات وحدة في المشروع ، والتكامل مع محلل تغطية JaCoCo موصوف في pom.xml . بالإضافة إلى ذلك ، يتم تكوين المكونات الإضافية لثلاثة أجهزة تحليل ثابتة: FindBugs و PMD و Checkstyle . إنه أكثر إثارة للاهتمام للتحقق من التحذيرات المتبقية.


إن محلل كود Java الثابت في نظام IntelliJ IDEA ليس أدنى من أدوات التحليل الثابتة المتخصصة ، ولكنه يفوقها في بعض النواحي. بالإضافة إلى ذلك ، تتوفر جميع إمكانات التحليل الثابت تقريبًا في Community Edition ، وهو IDE مفتوح المصدر مجانًا. على وجه الخصوص ، تنتج النسخة المجانية جميع التحذيرات الموضحة في هذه المقالة.


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


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


سأخبرك فقط عن بعض التحذيرات المثيرة للاهتمام.


1. أحادي زائد


كان هناك بالفعل 66 إيجابيات أحادية في المشروع. لكتابة +1 بدلاً من 1 فقط أحيانًا أريد أن أكون جميلًا. ومع ذلك ، في بعض الحالات ، تظهر علامة unary plus إذا بدلاً من += كتبوا =+ :


 int totalCharge1 = 0; while (atoms1.hasNext()) { totalCharge1 = +((IAtom) atoms1.next()).getFormalCharge(); } Iterator<IAtom> atoms2 = products.atoms().iterator(); int totalCharge2 = 0; while (atoms2.hasNext()) { totalCharge2 = +((IAtom) atoms2.next()).getFormalCharge(); } 

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


2. عدد صحيح يؤدي إلى كسور


خطأ مزعج إلى حد ما ، لكن المحللين الساكنين يجدونه جيدًا. هنا مثال :


 angle = 1 / 180 * Math.PI; 

لسوء الحظ ، تحولت الزاوية ليست درجة واحدة ، ولكن الصفر. خطأ مماثل :


 Integer c1 = features1.get(key); Integer c2 = features2.get(key); c1 = c1 == null ? 0 : c1; c2 = c2 == null ? 0 : c2; sum += 1.0 - Math.abs(c1 - c2) / (c1 + c2); // integer division in floating-point context 

يبدو أن كلا الرقمين c1 و c2 غير سالب ، مما يعني أن معامل الفرق لن يتجاوز المجموع. لذلك ، ستكون النتيجة 0 إذا كان كلا الرقمين غير صفري ، أو 1 إذا كان أحدهما يساوي 0.


3. استدعاء Class.getClass ()


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


 public <T extends ICDKObject> T ofClass(Class<T> intf, Object... objects) { try { if (!intf.isInterface()) throw new IllegalArgumentException("expected interface, got " + intf.getClass()); ... 

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


4. استدعاء toString () على مجموعة


هذا كلاسيكي من هذا النوع: لم يتم إعادة تعريف toString () للصفائف ، والنتيجة غير مجدية. عادة ما يمكن العثور عليها في رسائل التشخيص .


 int[] dim = {0, 0, 0}; ... return "Dim:" + dim + " SizeX:" + grid.length + " SizeY:" + grid[0].length + " SizeZ:"... 

من الصعب ملاحظة مشكلة العينين ، لأن هنا dim.toString() ضمني ، لكن سلسلة السلسلة تفوض إليها. يقترح على الفور إصلاح - التفاف في Arrays.toString(dim) .


5. قراءة المجموعة ولكن غير معبئة


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


 final Set<IBond> bondsToHydrogens = new HashSet<IBond>(); // ... 220  ,  bondsToHydrogens   ! for (IBond bondToHydrogen : bondsToHydrogens) //     sgroup.removeBond(bondToHydrogen); 

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


6. على العكس: نحن نملأ ، لكن لا تقرأ


حالات العكس هي أيضا ممكن. هنا مثال مع مجموعة :


 int[] tmp = new int[trueBits.length - 1]; System.arraycopy(trueBits, 0, tmp, 0, i); System.arraycopy(trueBits, i + 1, tmp, i, trueBits.length - i - 1); 

يعرف الاستقصاء أن الوسيطة الثالثة لطريقة المصفوفة تستخدم فقط لكتابة المصفوفة ، وبعد ذلك المصفوفة غير مستخدمة على الإطلاق. اذا حكمنا من خلال منطق الكود ، trueBits = tmp; تخطي خط trueBits = tmp; .


7. مقارنة عدد صحيح من قبل ==


هذا خطأ غادر ، لأنه يتم تخزين القيم الصغيرة لكائنات Integer مؤقتًا ، ويمكن أن يعمل كل شيء جيدًا حتى يتجاوز العدد 127 يومًا. قد لا تكون هذه المشكلة واضحة على الإطلاق :


 for (int a = 0; a < cliqueSize; a++) { for (int b = 0; b < vecSize; b += 3) { if (cliqueList.get(a) == compGraphNodes.get(b + 2)) { cliqueMapping.add(compGraphNodes.get(b)); cliqueMapping.add(compGraphNodes.get(b + 1)); } } } 

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


8. مكررة في الخريطة


في هذا الفحص ، الصورة تساوي ألف كلمة. انظر الخطأ ؟


هل هو أفضل؟


9. لا يتم استخدام نتيجة الأسلوب.


نتيجة بعض الطرق هي عدم استخدام ، وهذا ما تقارير IDEA بسهولة :


 currentChars.trim(); 

ربما ، يعني هذا currentChars = currentChars.trim(); . نظرًا لأن الجمل في Java غير قابلة للتغيير ، إذا لم يتم إعادة تعيين النتيجة ، فلن يحدث شيء. تم العثور عليها أيضًا ، على سبيل المثال ، str.substring(2) .


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


10. فروع التبديل غير قابلة للوصول


 // if character is out of scope don't if (c > 128) return 0; switch (c) { case '\u002d': // hyphen case '\u2012': // figure dash case '\u2013': // en-dash case '\u2014': // em-dash case '\u2212': // minus return '-'; // 002d default: return c; } 

نظرًا لأننا استبعدنا الأحرف التي تحتوي على رمز أكبر من 128 ، فإن الفروع \u2012-\u2212 قابلة للوصول. يبدو أنه لم يكن يستحق الاستثناء.


11. حالة غير قابلة للتحقيق


مشكلة رائعة للغاية في سلسلة الظروف :


 if (oxNum == 0) { if (hybrid.equals("sp3")) { ... } else if (hybrid.equals("sp2")) return 47; } else if (oxNum == 1 && hybrid.equals("sp3")) return 47; else if ((oxNum == 2 && hybrid.equals("sp3")) || (oxNum == 1 && hybrid.equals("sp2")) || (oxNum == 0 && hybrid.equals("sp"))) //     return 48; else if ((oxNum == 3 && hybrid.equals("sp3")) || (oxNum >= 2 && hybrid.equals("sp2")) || (oxNum >= 1 && hybrid.equals("sp"))) return 49; 

في المنطق الشرطي المعقد ، هذا ليس شيئًا غير مألوف: نحن نتحقق من حالة لا يمكن أن تكون حقيقية ، لأن شظاياها سبق أن تم فحصها أعلاه. هنا لدينا فرع منفصل oxNum == 0 ، وإلا فإننا نتحقق من oxNum == 0 && hybrid.equals("sp") ، وهو بالطبع لا يمكن أن يكون.


12. نكتب إلى مجموعة من طول الصفر


في بعض الأحيان ، ستلاحظ IntelliJ IDEA ما إذا كنت تكتب إلى صفيف خارج حجمها :


 Point3d points[] = new Point3d[0]; //    0  if (nwanted == 1) { points = new Point3d[1]; points[0] = new Point3d(aPoint); points[0].add(new Vector3d(length, 0.0, 0.0)); } else if (nwanted == 2) { //       —   points[0] = new Point3d(aPoint); points[0].add(new Vector3d(length, 0.0, 0.0)); points[1] = new Point3d(aPoint); points[1].add(new Vector3d(-length, 0.0, 0.0)); } 

13. التحقق من الطول بعد الوصول إلى الفهرس


مشكلة شائعة أخرى مع الإجراء ومرة ​​أخرى أثناء معالجة الأخطاء :


 public void setParameters(Object[] params) throws CDKException { if (params.length > 1) { throw new CDKException("..."); } if (!(params[0] instanceof Integer)) { //     throw new CDKException("The parameter must be of type Integer"); } if (params.length == 0) return; //      maxIterations = (Integer) params[0]; } 

في حالة وجود صفيف فارغ ، أراد مؤلف التعليمة البرمجية الخروج بهدوء ، ولكن بسبب التحقق ، سيخرج ، ويضرب بصوت عال ArrayIndexOutOfBoundsException. من الواضح أن ترتيب الشيكات خارج الترتيب.


14. تحقق من وجود لاغ بعد الوصول


ومرة أخرى ، يتم انتهاك ترتيب الإجراءات ، وهذه المرة باطلة :


 while (!line.startsWith("frame:") && input.ready() && line != null) { line = input.readLine(); logger.debug(lineNumber++ + ": ", line); } 

IDEA يكتب هذا line != null صحيح دائمًا. يحدث أن يكون الشيك زائداً حقًا ، لكن يبدو أن الكود هنا لاغٍ.


15. الانفصال بدلا من الاقتران


غالبًا ما يربك الأشخاص العوامل المنطقية AND و OR. مشروع CDK ليس استثناء :


 if (rStereo != 4 || pStereo != 4 || rStereo != 3 || pStereo != 3) { ... } 

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


16. مرة أخرى الانفصال بدلا من الاقتران


خطأ مماثل ، ولكن اشتعلت بواسطة رسالة أخرى:


 if (getFirstMapping() != null || !getFirstMapping().isEmpty()) { ... } 

لا يمكننا الوصول إلى الجانب الأيمن إلا إذا عاد getFirstMapping() null ، ولكن في هذه الحالة ، نضمن لك NullPointerException ، والذي تحذر IDEA منه. بالمناسبة ، نحن نعتمد هنا على ثبات نتائج أسلوب getFirstMapping() . في بعض الأحيان نستخدم الاستدلال ، ولكن يتم تحليل الاستقرار مباشرة هنا. نظرًا لأن الفصل نهائي ، لا يمكن تجاوز الطريقة. return firstSolution.isEmpty() ? null : firstSolution IDEA بفحص جسدها return firstSolution.isEmpty() ? null : firstSolution return firstSolution.isEmpty() ? null : firstSolution وتحدد أن الاستقرار يأتي إلى ثبات طريقة Map#isEmpty ، والتي تم Map#isEmpty سابقًا على أنها مستقرة.


17. التسلسل الهرمي للواجهات و instof


عند التحقق من وجود كائن ينتمي إلى أي واجهة ، لا تنس أن الواجهات يمكن أن ترث من بعضها البعض :


 if (object instanceof IAtomContainer) { root = convertor.cdkAtomContainerToCMLMolecule((IAtomContainer) object); } else if (object instanceof ICrystal) { root = convertor.cdkCrystalToCMLMolecule((ICrystal) object); } ... 

تقوم واجهة ICrystal واجهة IAtomContainer ، لذلك من الواضح أن الفرع الثاني IAtomContainer إليه: إذا جاءت البلورة ، فسوف تقع في الفرع الأول.


18. عبور قائمة فارغة


ربما لا يكون مؤلف هذا الرمز على دراية بلغة جافا:


 List<Integer> posNumList = new ArrayList<Integer>(size); for (int i = 0; i < posNumList.size(); i++) { posNumList.add(i, 0); } 

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


19. لا تنس تهيئة الحقول


المحلل بطريقة خاصة يتحقق البنائين ، مع الأخذ في الاعتبار مهيئات الحقل. بفضل هذا ، تم العثور على مثل هذا الخطأ :


 public class IMatrix { public double[][] realmatrix; public double[][] imagmatrix; public int rows; public int columns; public IMatrix(Matrix m) { rows = m.rows; columns = m.columns; int i, j; for (i = 0; i < rows; i++) for (j = 0; j < columns; j++) { realmatrix[i][j] = m.matrix[i][j]; // NullPointerException  imagmatrix[i][j] = 0d; } } } 

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


20. لا تكرر مرتين


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


 if (commonAtomCount > vfMCSSize && commonAtomCount > vfMCSSize) { return true; } 

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


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

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


All Articles