10 bug terbaik di proyek Java untuk 2019


2019 akan segera berakhir, dan tim PVS-Studio merangkum hasil tahun yang akan datang. Pada awal 2019, kami memperluas kemampuan penganalisa dengan mendukung bahasa Jawa. Oleh karena itu, daftar publikasi kami tentang pengecekan proyek terbuka diisi dengan ulasan proyek Jawa. Banyak kesalahan ditemukan sepanjang tahun, dan kami memutuskan untuk menyiapkan 10 besar yang paling menarik dari mereka.


Tempat kesepuluh: byte ikonik


Sumber: Analisis kode sumber kerangka kerja RPC Apache Dubbo oleh analyzer statis PVS-Studio

Ekspresi V6007 'endKey [i] <0xff' selalu benar. OptionUtil.java (32)

public static final ByteSequence prefixEndOf(ByteSequence prefix) { byte[] endKey = prefix.getBytes().clone(); for (int i = endKey.length - 1; i >= 0; i--) { if (endKey[i] < 0xff) { // <= endKey[i] = (byte) (endKey[i] + 1); return ByteSequence.from(Arrays.copyOf(endKey, i + 1)); } } return ByteSequence.from(NO_PREFIX_END); } 

Banyak programmer percaya bahwa tipe yang disebut byte akan tidak ditandatangani. Dan memang, seringkali dalam bahasa yang berbeda inilah masalahnya. Misalnya, dalam C #, tipe byte tidak ditandai. Di Jawa, ini bukan masalahnya.

Dalam kondisi endKey [i] <0xff, penulis metode membandingkan variabel tipe byte dengan angka 255 (0xff) yang diwakili dalam representasi heksadesimal. Rupanya, saat menulis metode, pengembang lupa bahwa kisaran nilai-nilai tipe byte di Jawa adalah [-128, 127]. Kondisi ini selalu benar, jadi untuk loop akan selalu memproses hanya elemen terakhir dari array endKey .

Tempat kesembilan: dua dalam satu


Sumber: PVS-Studio untuk Java dikirim ke jalur. Perhentian berikutnya adalah Elasticsearch

Ekspresi V6007 '(int) x <0' selalu salah. BCrypt.java (429)

V6025 Kemungkinan indeks '(int) x' di luar batas. BCrypt.java (431)

 private static byte char64(char x) { if ((int)x < 0 || (int)x > index_64.length) return -1; return index_64[(int)x]; } 

Hari ini kami memiliki penawaran khusus! Dua kesalahan dalam satu metode sekaligus. Penyebab kesalahan pertama adalah tipe char , yang tidak ditandatangani di Jawa, itulah sebabnya kondisi (int) x <0 selalu salah. Kesalahan kedua adalah dangkal keluar dari batas array index_64 ketika (int) x == index_64.length . Situasi ini dimungkinkan karena kondisi (int) x> index_64.length . Untuk menghilangkan batas array, perlu untuk mengganti kondisi '>' dengan '> ='. Kondisi yang benar adalah: (int) x> = index_64.length .

Tempat kedelapan: keputusan dan konsekuensinya


Sumber: Analisis Kode Platform CUBA dengan PVS-Studio

Ekspresi V6007 'previousMenuItemFlatIndex> = 0' selalu benar. CubaSideMenuWidget.java (328)

 protected MenuItemWidget findNextMenuItem(MenuItemWidget currentItem) { List<MenuTreeNode> menuTree = buildVisibleTree(this); List<MenuItemWidget> menuItemWidgets = menuTreeToList(menuTree); int menuItemFlatIndex = menuItemWidgets.indexOf(currentItem); int previousMenuItemFlatIndex = menuItemFlatIndex + 1; if (previousMenuItemFlatIndex >= 0) { // <= return menuItemWidgets.get(previousMenuItemFlatIndex); } return null; } 

Penulis metode findNextMenuItem ingin menyingkirkan -1 yang dikembalikan oleh metode indexOf jika daftar menuItemWidgets tidak mengandung currentItem . Untuk melakukan ini, ia menambahkan satu ke hasil indexOf (variabel menuItemFlatIndex ) dan menyimpan nilai yang dihasilkan dalam variabelMenuItemFlatIndex sebelumnya , yang selanjutnya digunakan dalam metode ini. Solusi untuk masalah -1 ini tidak berhasil karena menyebabkan beberapa kesalahan sekaligus:

  • return null code tidak akan pernah dieksekusi, karena ekspresi beforeMenuItemFlatIndex > = 0 selalu benar, yang berarti bahwa pengembalian dari metode findNextMenuItem akan selalu terjadi di dalam if ;
  • IndexOutOfBoundsException akan dilemparkan ketika daftar menuItemWidgets kosong, karena elemen pertama dari daftar kosong akan diakses;
  • pengecualian IndexOutOfBoundsException akan terjadi ketika argumen currentItem adalah yang terakhir dalam daftar menuItemWidget .

Tempat ketujuh: membuat file dari ketiadaan


Sumber: Huawei Cloud: hari ini berawan di PVS-Studio

V6008 Potensi null dereference dari 'dataTmpFile'. CacheManager.java (91)

 @Override public void putToCache(PutRecordsRequest putRecordsRequest) { .... if (dataTmpFile == null || !dataTmpFile.exists()) { try { dataTmpFile.createNewFile(); // <= } catch (IOException e) { LOGGER.error("Failed to create cache tmp file, return.", e); return; } } .... } 

Saat menulis metode putToCache, programmer membuat kesalahan ketik dalam kondisi dataTmpFile == null || ! dataTmpFile.exists () sebelum membuat file dataTmpFile.createNewFile () baru. Salah ketik adalah penggunaan operator '==' bukan '! ='. Kesalahan ketik ini akan melempar NullPointerException saat memanggil metode createNewFile . Kondisi setelah mengoreksi kesalahan ketik terlihat seperti ini:

 if (dataTmpFile != null || !dataTmpFile.exists()) 

“Kesalahan ditemukan, diperbaiki. Anda bisa santai, ”Anda akan berpikir. Tapi bagaimanapun caranya!

Setelah memperbaiki satu kesalahan, kami menemukan yang lain. Sekarang, NullPointerException dapat terjadi saat memanggil dataTmpFile.exists () . Sekarang, untuk menyingkirkan pengecualian, perlu untuk mengganti operator '||' dalam kondisi di '&&'. Kondisi di mana semua kesalahan hilang adalah sebagai berikut:

 if (dataTmpFile != null && !dataTmpFile.exists()) 

Tempat keenam: kesalahan logis yang sangat aneh


Sumber: PVS-Studio untuk Java

V6007 [CWE-570] Ekspresi '"0". Equals (text)' selalu salah. ConvertIntegerToDecimalPredicate.java 46

 public boolean satisfiedBy(@NotNull PsiElement element) { .... @NonNls final String text = expression.getText().replaceAll("_", ""); if (text == null || text.length() < 2) { return false; } if ("0".equals(text) || "0L".equals(text) || "0l".equals(text)) {// <= return false; } return text.charAt(0) == '0'; } 

Metode ini menarik karena mengandung kesalahan logis yang jelas. Jika metode puas dengan tidak mengembalikan nilai setelah yang pertama jika , diketahui bahwa string teks terdiri dari setidaknya dua karakter. Karena itu, centang pertama "0". Sama dengan (teks) di berikutnya jika tidak ada artinya. Apa yang sebenarnya dimaksud pengembang tetap menjadi misteri.

Tempat kelima: ini giliran!


Sumber: PVS-Studio mengunjungi Apache Hive

V6034 Shift dengan nilai 'bitShiftsInWord - 1' bisa tidak konsisten dengan ukuran tipe: 'bitShiftsInWord - 1' = [-1 ... 30]. UnsignedInt128.java (1791)

 private void shiftRightDestructive(int wordShifts, int bitShiftsInWord, boolean roundUp) { if (wordShifts == 0 && bitShiftsInWord == 0) { return; } assert (wordShifts >= 0); assert (bitShiftsInWord >= 0); assert (bitShiftsInWord < 32); if (wordShifts >= 4) { zeroClear(); return; } final int shiftRestore = 32 - bitShiftsInWord; // check this because "123 << 32" will be 123. final boolean noRestore = bitShiftsInWord == 0; final int roundCarryNoRestoreMask = 1 << 31; final int roundCarryMask = (1 << (bitShiftsInWord - 1)); // <= .... } 

Dengan argumen input wordShifts = 3 dan bitShiftsInWord = 0 , variabel roundCarryMask , yang menyimpan hasil dari pergeseran bit (1 << (bitShiftsInWord - 1)) , akan berubah menjadi angka negatif. Mungkin pengembang tidak mengharapkan perilaku ini.

Tempat keempat: apakah pengecualian akan berjalan-jalan?


Sumber: PVS-Studio mengunjungi Apache Hive

V6051 Penggunaan pernyataan 'kembali' di blok 'akhirnya' dapat menyebabkan hilangnya pengecualian yang tidak ditangani. ObjectStore.java (9080)

 private List<MPartitionColumnStatistics> getMPartitionColumnStatistics(....) throws NoSuchObjectException, MetaException { boolean committed = false; try { .... /*some actions*/ committed = commitTransaction(); return result; } catch (Exception ex) { LOG.error("Error retrieving statistics via jdo", ex); if (ex instanceof MetaException) { throw (MetaException) ex; } throw new MetaException(ex.getMessage()); } finally { if (!committed) { rollbackTransaction(); return Lists.newArrayList(); } } } 

Deklarasi metode getMPartitionColumnStatistics berbohong kepada kami, mengatakan bahwa itu bisa melempar pengecualian. Ketika pengecualian terjadi dalam percobaan , variabel yang dikomit tetap salah , oleh karena itu, di blok akhirnya , pernyataan kembali mengembalikan nilai dari metode, dan semua pengecualian yang dilemparkan hilang dan tidak dapat diproses di luar metode. Dengan demikian, setiap pengecualian yang muncul dalam metode ini tidak akan pernah bisa keluar darinya.

Tempat ketiga: Saya memutar, berputar, saya ingin mendapatkan topeng baru


Sumber: PVS-Studio mengunjungi Apache Hive

V6034 Shift dengan nilai 'j' bisa tidak konsisten dengan ukuran tipe: 'j' = [0 ... 63]. IoTrace.java (272)

 public void logSargResult(int stripeIx, boolean[] rgsToRead) { .... for (int i = 0, valOffset = 0; i < elements; ++i, valOffset += 64) { long val = 0; for (int j = 0; j < 64; ++j) { int ix = valOffset + j; if (rgsToRead.length == ix) break; if (!rgsToRead[ix]) continue; val = val | (1 << j); // <= } .... } .... } 

Kesalahan lain terkait dengan pergeseran bitwise, tetapi kali ini tidak hanya dia terlibat dalam kasus ini. Di dalam untuk loop, variabel j [0 ... 63] digunakan sebagai penghitung loop. Penghitung ini terlibat dalam sedikit pergeseran 1 << j . Tidak ada yang menandakan masalah, bagaimanapun, integer literal '1' dari tipe int (nilai 32-bit) ikut bermain di sini. Oleh karena itu, hasil dari bit shift akan mulai diulang setelah j lebih dari 31. Jika perilaku yang dijelaskan tidak diinginkan, maka unit harus direpresentasikan sebagai panjang , misalnya, 1L << j atau (panjang) 1 << j .

Tempat Kedua: Urutan Inisialisasi


Sumber: Huawei Cloud: hari ini berawan di PVS-Studio

Siklus inisialisasi Kelas V6050 hadir. Inisialisasi 'INSTANCE' muncul sebelum inisialisasi 'LOG'. UntrustedSSL.java (32), UntrustedSSL.java (59), UntrustedSSL.java (33)

 public class UntrustedSSL { private static final UntrustedSSL INSTANCE = new UntrustedSSL(); private static final Logger LOG = LoggerFactory.getLogger(UntrustedSSL.class); .... private UntrustedSSL() { try { .... } catch (Throwable t) { LOG.error(t.getMessage(), t); // <= } } } 

Urutan bidang mana yang dideklarasikan dalam kelas adalah penting karena bidang diinisialisasi dalam urutan mereka dideklarasikan. Namun, ketika mereka melupakannya, kesalahan kecil terjadi, seperti ini.

Penganalisis menunjukkan bahwa bidang LOG statis direferensikan dalam konstruktor ketika diinisialisasi ke nol , yang mengarah ke NullPointerException -> rantai pengecualian ExceptionInInitializerError .

"Mengapa, pada saat panggilan konstruktor, apakah bidang LOG statis nol ?" Anda bertanya.

Pengecualian ExceptionInInitializerError adalah petunjuk. Faktanya adalah bahwa konstruktor ini digunakan untuk menginisialisasi bidang statis INSTANCE dideklarasikan di kelas lebih awal daripada bidang LOG . Oleh karena itu, pada saat panggilan konstruktor, bidang LOG masih belum diinisialisasi. Agar kode berfungsi dengan benar, perlu menginisialisasi bidang LOG sebelum memanggil konstruktor.

Tempat pertama: pemrograman berorientasi copy-paste


Sumber: Apache Hadoop Code Quality: produksi VS test

V6072 Dua fragmen kode serupa ditemukan. Mungkin, ini adalah kesalahan ketik dan variabel 'localFiles' harus digunakan daripada 'localArchives'. LocalDistributedCacheManager.java (183), LocalDistributedCacheManager.java (178), LocalDistributedCacheManager.java (176), LocalDistributedCacheManager.java (181)

 public synchronized void setup(JobConf conf, JobID jobId) throws IOException { .... // Update the configuration object with localized data. if (!localArchives.isEmpty()) { conf.set(MRJobConfig.CACHE_LOCALARCHIVES, StringUtils .arrayToString(localArchives.toArray(new String[localArchives // <= .size()]))); } if (!localFiles.isEmpty()) { conf.set(MRJobConfig.CACHE_LOCALFILES, StringUtils .arrayToString(localFiles.toArray(new String[localArchives // <= .size()]))); } .... } 

Dan tempat pertama diambil dengan copy-paste, atau lebih tepatnya, kesalahan yang muncul karena kecerobohan orang yang melakukan hal berdosa ini. Sangat mungkin bahwa yang kedua jika dibuat dengan copy-paste yang pertama dengan penggantian variabel:

  • Arsip lokal tentang Berkas lokal ;
  • MRJobConfig.CACHE_LOCALARCHIVES di MRJobConfig.CACHE_LOCALFILES .

Namun, bahkan dengan operasi sederhana seperti itu, kesalahan telah dibuat, karena variabel localArchives masih digunakan di baris kedua jika di penganalisa kedua, meskipun penggunaan localFiles kemungkinan besar tersirat.

Kesimpulan


Koreksi kesalahan yang ditemukan pada tahap pengembangan selanjutnya atau setelah rilis proyek membutuhkan sumber daya yang signifikan. Analyzer statis PVS-Studio menyederhanakan deteksi kesalahan saat menulis kode, yang secara signifikan mengurangi jumlah sumber daya yang dihabiskan untuk memperbaikinya. Penggunaan analisa yang konstan telah menyederhanakan kehidupan pengembang dari banyak perusahaan . Jika Anda ingin memprogram dengan senang hati, cobalah penganalisa kami.

Tim kami tidak akan berhenti di situ dan akan terus meningkatkan dan meningkatkan alat analisis. Harapkan diagnostik dan artikel baru dengan bug yang lebih menarik tahun depan.

Saya melihat Anda suka petualangan! Pertama, 10 kesalahan teratas dalam proyek C # untuk 2019 dimenangkan, dan sekarang Java dapat diatasi! Selamat datang di level berikutnya dalam artikel tentang kesalahan terbaik 2019 dalam proyek C ++ .





Jika Anda ingin berbagi artikel ini dengan audiens yang berbahasa Inggris, silakan gunakan tautan ke terjemahan: Valery Komarov. Top 10 Bug Ditemukan di Proyek Java pada tahun 2019 .

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


All Articles