قررت اختبار محلل كود 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);
يبدو أن كلا الرقمين 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>();
من الواضح ملء مجرد غاب. يكون للمحللات الثابتة اختبارات أبسط تشير إلى متغير غير مستخدم ، ولكن يتم استخدام المتغير هنا ، لذا فهي صامتة. نحتاج إلى فحص أكثر ذكاءً يعرف عن المجموعات.
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. فروع التبديل غير قابلة للوصول
نظرًا لأننا استبعدنا الأحرف التي تحتوي على رمز أكبر من 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")))
في المنطق الشرطي المعقد ، هذا ليس شيئًا غير مألوف: نحن نتحقق من حالة لا يمكن أن تكون حقيقية ، لأن شظاياها سبق أن تم فحصها أعلاه. هنا لدينا فرع منفصل oxNum == 0
، وإلا فإننا نتحقق من oxNum == 0 && hybrid.equals("sp")
، وهو بالطبع لا يمكن أن يكون.
12. نكتب إلى مجموعة من طول الصفر
في بعض الأحيان ، ستلاحظ IntelliJ IDEA ما إذا كنت تكتب إلى صفيف خارج حجمها :
Point3d points[] = new Point3d[0];
13. التحقق من الطول بعد الوصول إلى الفهرس
مشكلة شائعة أخرى مع الإجراء ومرة أخرى أثناء معالجة الأخطاء :
public void setParameters(Object[] params) throws CDKException { if (params.length > 1) { throw new CDKException("..."); } if (!(params[0] instanceof Integer)) {
في حالة وجود صفيف فارغ ، أراد مؤلف التعليمة البرمجية الخروج بهدوء ، ولكن بسبب التحقق ، سيخرج ، ويضرب بصوت عال 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];
على الرغم من حقيقة أن الحقول عامة ، إلا أنه لا يمكن لأي شخص هنا بالتأكيد إسفينها وتهيئتها أمام المُنشئ. لذلك ، تصدر IDEA بجرأة تحذيرًا من أن الوصول إلى عنصر صفيف سيرفع NullPointerException.
20. لا تكرر مرتين
الظروف المتكررة تحدث أيضا في كثير من الأحيان. هنا مثال :
if (commonAtomCount > vfMCSSize && commonAtomCount > vfMCSSize) { return true; }
هذه الأخطاء غدائية ، لأنك لا تعرف أبدًا ، الشرط الثاني لا لزوم له ، أو أن المؤلف أراد التحقق من شيء آخر. إذا لم يتم إصلاح هذا فورًا ، فقد يكون من الصعب تحديد ذلك. هذا سبب آخر لماذا يجب استخدام التحليل الثابت باستمرار.
أبلغت عن بعض هذه الأخطاء في برنامج تتبع الأخطاء في المشروع . من الغريب أنه عندما قام مؤلفو المشروع بإصلاح جزء ما ، استخدموا هم أنفسهم محلل IntelliJ IDEA ، ووجدوا مشكلات أخرى لم أكتب عنها ، وبدأوا أيضًا في حلها . أعتقد أن هذه علامة جيدة: أدرك المؤلفون أهمية التحليل الثابت.