Belum lama ini, saya mempelajari output dari penganalisa statis IntelliJ IDEA untuk kode Java dan menemukan sebuah kasus yang menarik. Karena fragmen kode yang sesuai bukan open source, saya menganonimkannya dan melepaskan ikatannya dari dependensi eksternal. Kami berasumsi bahwa dia terlihat seperti ini:
private static List<Integer> process(Map<String, Integer> options, List<String> inputs) { List<Integer> res = new ArrayList<>(); int cur = -1; for (String str : inputs) { if (str.startsWith("-")) if (options.containsKey(str)) { if (cur == -1) cur = options.get(str); } else if (options.containsKey("+" + str)) { if (cur == -1) cur = res.isEmpty() ? -1 : res.remove(res.size() - 1); if (cur != -1) res.add(cur + str.length()); } } return res; }
Kode itu seperti kode, sesuatu sedang ditransformasikan, ada yang dilakukan, tetapi penganalisa statis tidak menyukainya. Di sini kita melihat dua peringatan:

Pada ekspresi res.isEmpty()
IDE mengatakan bahwa kondisinya selalu benar, dan pada saat cur
bahwa tugas tidak berarti, karena nilai yang sama sudah ada dalam variabel ini. Sangat mudah untuk melihat bahwa masalah penugasan adalah konsekuensi langsung dari masalah pertama. Jika res.isEmpty()
benar res.isEmpty()
benar selalu benar, maka string dikurangi menjadi
if (cur == -1) cur = -1;
Ini memang berlebihan. Tetapi mengapa ungkapan itu selalu benar? Bagaimanapun, res
adalah daftar, itu diisi dalam siklus yang sama. Jumlah iterasi dari loop dan cabang mana yang kita masuki tergantung pada parameter input yang tidak diketahui oleh IDE. Kita dapat menambahkan elemen ke res
pada iterasi sebelumnya, dan kemudian daftar tidak akan kosong.
Saya melihat kode ini untuk pertama kalinya dan menghabiskan banyak waktu untuk menangani kasus ini. Pada awalnya, saya hampir yakin bahwa saya menemukan bug di analisa, dan saya harus memperbaikinya. Mari kita lihat apakah ini benar.
Pertama, kami akan menandai semua baris di mana kondisi metode berubah. Ini adalah perubahan ke variabel cur
atau perubahan ke daftar res
:
private static List<Integer> process(Map<String, Integer> options, List<String> inputs) { List<Integer> res = new ArrayList<>(); int cur = -1; for (String str : inputs) { if (str.startsWith("-")) if (options.containsKey(str)) { if (cur == -1) cur = options.get(str);
Baris 'A'
dan 'B'
( 'B'
adalah cabang pertama dari pernyataan kondisi) mengubah variabel cur
, 'D'
mengubah daftar, dan 'C'
(cabang kedua dari pernyataan kondisional) mengubah daftar dan variabel cur
. Penting bagi kami apakah cur
-1 terletak di dan apakah daftar itu kosong. Artinya, Anda perlu memantau empat negara:

String 'A'
berubah saat ini jika ada -1
sebelum itu. Dan kita tidak tahu apakah hasilnya -1
atau tidak. Oleh karena itu, dua opsi dimungkinkan:

String 'B'
juga berfungsi hanya jika cur
adalah -1
. Pada saat yang sama, seperti yang telah kita perhatikan, pada prinsipnya, dia tidak melakukan apa pun. Namun demikian kami mencatat bahwa tulang rusuk ini untuk melengkapi gambar:

String 'C'
, seperti yang sebelumnya, bekerja dengan cur == -1
dan mengubahnya secara sewenang-wenang (seperti 'A'
). Tetapi pada saat yang sama, itu masih bisa mengubah daftar yang tidak kosong menjadi kosong, atau membiarkan tidak kosong jika ada lebih dari satu elemen.

Akhirnya, string 'D'
meningkatkan ukuran daftar: itu dapat berubah kosong menjadi tidak kosong, atau meningkatkan tidak kosong. Itu tidak dapat mengubah kosong menjadi kosong:

Apa yang ini berikan pada kita? Tidak ada sama sekali. Benar-benar tidak dapat dipahami mengapa kondisi res.isEmpty()
selalu benar.
Sebenarnya, kami salah memulai. Dalam hal ini, tidak cukup untuk memantau keadaan masing-masing variabel secara terpisah. Di sini keadaan yang berkorelasi memainkan peran penting. Untungnya, karena kenyataan bahwa 2+2 = 2*2
, kami juga hanya memiliki empat diantaranya:

Dengan batas ganda, saya menandai keadaan awal yang kita miliki saat memasukkan metode. Baiklah, coba lagi. 'A'
mengubah atau menyimpan cur
untuk res
apa pun, res
tidak berubah:

'B'
hanya bekerja dengan cur == -1 && res.isEmpty()
dan tidak melakukan apa pun. Tambahkan:

'C'
hanya bekerja dengan cur == -1 && !res.isEmpty()
. Pada saat yang sama, baik cur
dan res
berubah secara sewenang-wenang: setelah 'C'
kita berakhir dalam keadaan apa pun:

Akhirnya, 'D'
dapat mulai di cur != -1 && res.isEmpty()
dan membuat daftar tidak kosong, atau mulai di cur != -1 && !res.isEmpty()
dan tetap di sana:

Pada pandangan pertama tampaknya semakin memburuk: grafik menjadi lebih rumit, dan tidak jelas bagaimana menggunakannya. Namun faktanya, kami dekat dengan solusi. Panah sekarang menunjukkan seluruh kemungkinan aliran eksekusi metode kami. Karena kita tahu dari mana kita mulai, mari kita berjalan di sepanjang panah:

Dan di sini hal yang sangat menarik terungkap. Kami tidak bisa sampai ke sudut kiri bawah. Dan karena kita tidak bisa masuk ke dalamnya, itu berarti kita tidak bisa berjalan di sepanjang panah 'C'
. Artinya, baris 'C'
benar 'C'
benar tidak dapat dijangkau, dan 'B'
dapat dieksekusi. Ini hanya mungkin jika kondisi res.isEmpty()
memang selalu benar! IntelliJ IDEA sepenuhnya benar. Maaf, penganalisa, sia-sia saya pikir Anda buggy. Anda begitu pintar sehingga sulit bagi saya, orang biasa, untuk menyusul Anda.
Analiser kami tidak memiliki teknologi "sensasi" kecerdasan buatan, tetapi menggunakan pendekatan analisis aliran kontrol dan analisis aliran data, yang berusia tidak kurang dari setengah abad. Namun demikian, ia terkadang menarik kesimpulan yang sangat tidak sepele. Namun, ini bisa dimengerti: untuk waktu yang lama lebih baik membuat grafik dan berjalan menggunakan mesin daripada dengan orang. Ada masalah penting yang belum terselesaikan: tidak cukup hanya memberi tahu seseorang bahwa ia memiliki kesalahan program. Otak silikon harus menjelaskan kepada biologis mengapa ia memutuskan demikian, dan agar otak biologis mengerti. Jika seseorang memiliki ide cemerlang tentang cara melakukan ini, saya akan senang mendengar dari Anda. Jika Anda siap untuk mewujudkan ide-ide Anda sendiri, tim kami tidak akan menolak untuk bekerja sama dengan Anda!
Salah satu tes penerimaan adalah sebelum Anda: untuk contoh ini, penjelasan harus dihasilkan secara otomatis. Ini bisa berupa teks, grafik, pohon, gambar dengan segel - apa pun, jika hanya orang yang bisa mengerti.
Pertanyaannya tetap terbuka, apa yang dimaksud oleh penulis metode ini, dan bagaimana kode tersebut seharusnya terlihat. Mereka yang bertanggung jawab atas subsistem memberitahu saya bahwa bagian ini agak ditinggalkan, dan mereka sendiri tidak tahu cara memperbaikinya atau lebih baik menghapusnya sama sekali.