Saya diminta untuk menulis artikel ini dengan sejumlah besar bahan tentang analisis statis, yang semakin sering menarik perhatian saya. Pertama, ini adalah
blog PVS-studio , yang secara aktif mempromosikan dirinya di Habré dengan bantuan ulasan kesalahan yang ditemukan oleh alat mereka dalam proyek-proyek open source. Baru-baru ini, studio-PVS mengimplementasikan
dukungan untuk Java , dan, tentu saja, para pengembang IntelliJ IDEA, yang penganalisis bawaannya mungkin yang paling canggih untuk Jawa saat ini,
tidak bisa menjauh .
Saat membaca ulasan semacam itu, ada perasaan bahwa kita berbicara tentang ramuan ajaib: klik tombolnya, dan ini dia - daftar cacat di depan mata Anda. Tampaknya seiring dengan peningkatan analisis, semakin banyak bug akan secara otomatis ditemukan, dan produk yang dipindai oleh robot ini akan menjadi lebih baik dan lebih baik, tanpa upaya dari pihak kami.
Tetapi tidak ada ramuan ajaib. Saya ingin berbicara tentang sesuatu yang biasanya tidak disebutkan dalam posting seperti "ini adalah hal-hal yang dapat ditemukan robot kami": apa yang tidak dapat dilakukan oleh analis, apa peran dan tempat sebenarnya mereka dalam proses pengiriman perangkat lunak, dan bagaimana menerapkannya dengan benar.
Ratchet (sumber: Wikipedia ).Apa analisa statis tidak akan pernah bisa
Apa, dari sudut pandang praktis, analisis kode sumber? Kami mengirimkan beberapa sumber ke input, dan pada output dalam waktu singkat (jauh lebih pendek daripada uji coba) kami mendapatkan beberapa informasi tentang sistem kami. Keterbatasan mendasar dan matematis tidak dapat diatasi adalah bahwa kita hanya bisa mendapatkan kelas informasi yang agak sempit dengan cara ini.
Contoh paling terkenal dari masalah yang tidak dapat diselesaikan dengan bantuan analisis statis adalah
masalah shutdown : ini adalah teorema yang membuktikan bahwa tidak mungkin untuk mengembangkan algoritma umum yang akan ditentukan oleh kode sumber suatu program apakah akan berulang atau berakhir dalam waktu yang terbatas. Perpanjangan teorema ini adalah teorema
Rice, yang menyatakan bahwa untuk properti non-sepele dari fungsi yang dapat dihitung, menentukan apakah program arbitrer menghitung fungsi dengan properti ini adalah masalah algoritmik yang tidak dapat dipecahkan. Sebagai contoh, adalah mustahil untuk menulis analisa yang ditentukan oleh kode sumber apa saja apakah program yang dianalisis adalah implementasi dari algoritma yang menghitung, katakanlah, kuadrat dari sebuah integer.
Dengan demikian, fungsi analisis statis memiliki keterbatasan yang tidak dapat diatasi. Dalam semua kasus, penganalisa statis tidak akan pernah dapat menentukan hal-hal seperti, misalnya, terjadinya "pengecualian penunjuk nol" dalam bahasa yang dapat dibatalkan, atau dalam semua kasus, menentukan terjadinya "atribut tidak ditemukan" dalam bahasa yang diketik secara dinamis. Yang dapat dilakukan oleh penganalisa statis paling canggih adalah menyoroti kasus-kasus tertentu, yang jumlah di antara semua masalah yang mungkin terjadi dengan kode sumber Anda adalah, tanpa berlebihan, setetes dalam ember.
Analisis statis bukanlah pencarian bug
Kesimpulannya mengikuti dari uraian di atas: analisis statis bukan cara mengurangi jumlah cacat dalam suatu program. Saya akan menyatakan: ketika pertama kali diterapkan pada proyek Anda, ia akan menemukan tempat "sibuk" dalam kode, tetapi kemungkinan besar tidak akan menemukan cacat yang memengaruhi kualitas program Anda.
Contoh-contoh cacat yang ditemukan secara otomatis oleh analisis sangat mengesankan, tetapi kita tidak boleh lupa bahwa contoh-contoh ini ditemukan dengan memindai sekumpulan basis kode besar. Dengan prinsip yang sama, cracker dengan kemampuan untuk menyebutkan beberapa kata sandi sederhana pada sejumlah besar akun akhirnya menemukan akun-akun yang memiliki kata sandi sederhana.
Apakah ini berarti bahwa analisis statis tidak perlu diterapkan? Tentu tidak! Dan untuk alasan yang sama persis, ada baiknya memeriksa setiap kata sandi baru untuk sampai ke daftar berhenti kata sandi "sederhana".
Analisis statis lebih dari pencarian bug
Faktanya, tugas-tugas yang secara praktis diselesaikan dengan analisis jauh lebih luas. Memang, secara umum, analisis statis adalah setiap verifikasi sumber yang dilakukan sebelum diluncurkan. Berikut beberapa hal yang dapat Anda lakukan:
- Verifikasi gaya pengkodean dalam arti luas. Ini termasuk memeriksa pemformatan, dan mencari penggunaan tanda kurung kosong / ekstra, menetapkan nilai ambang untuk metrik seperti jumlah baris / kompleksitas siklomatik dari metode, dll. - semua itu berpotensi menyebabkan kode sulit dibaca dan dipelihara. Di Jawa alat seperti itu adalah Checkstyle, dengan Python - flake8. Program kelas ini biasanya disebut linter.
- Tidak hanya kode yang dapat dieksekusi yang dapat dianalisis. File sumber daya seperti JSON, YAML, XML, .properties dapat (dan seharusnya!) Secara otomatis diperiksa validitasnya. Lagipula, lebih baik untuk mengetahui bahwa karena beberapa kutipan tidak berpasangan, struktur JSON dilanggar pada tahap awal pemeriksaan Permintaan Tarik otomatis daripada saat menjalankan tes atau dalam waktu Jalankan? Alat yang relevan tersedia: misalnya, YAMLlint , JSONLint .
- Kompilasi (atau parsing untuk bahasa pemrograman dinamis) juga merupakan bentuk analisis statis. Sebagai aturan, kompiler dapat mengeluarkan peringatan masalah pensinyalan dengan kualitas kode sumber, dan mereka tidak boleh diabaikan.
- Terkadang kompilasi tidak hanya kompilasi kode yang dapat dieksekusi. Misalnya, jika Anda memiliki dokumentasi dalam format AsciiDoctor , maka pada saat mengubahnya menjadi HTML / PDF, pawang AsciiDoctor ( plugin Maven ) dapat memberikan peringatan, misalnya, tentang tautan internal yang terputus. Dan ini adalah alasan bagus untuk tidak menerima Permintaan Tarik dengan perubahan pada dokumentasi.
- Pemeriksaan ejaan juga merupakan bentuk analisis statis. Utilitas aspell mampu memeriksa ejaan tidak hanya dalam dokumentasi, tetapi juga dalam kode sumber program (komentar dan literal) dalam berbagai bahasa pemrograman, termasuk C / C ++, Java dan Python. Kesalahan pengejaan di antarmuka pengguna atau dokumentasi juga cacat!
- Tes konfigurasi (untuk apa - lihat ini dan laporan ini ), meskipun dijalankan dalam lingkungan runtime untuk tes unit seperti pytest, sebenarnya juga merupakan semacam analisis statis, karena mereka tidak mengeksekusi kode sumber selama pelaksanaannya .
Seperti yang Anda lihat, pencarian bug dalam daftar ini mengambil peran paling tidak penting, dan semua yang lain tersedia melalui penggunaan alat open source gratis.
Manakah dari jenis analisis statis yang harus digunakan dalam proyek Anda? Tentu saja, semuanya, semakin banyak - semakin baik! Hal utama adalah mengimplementasikannya dengan benar, yang akan dibahas lebih lanjut.
Jalur pengiriman sebagai filter multi-tahap dan analisis statis sebagai kaskade pertama
Metafora klasik untuk integrasi berkelanjutan adalah jalur pipa melalui mana perubahan mengalir - dari mengubah kode sumber hingga dikirim ke produksi. Urutan standar langkah-langkah dalam pipa ini adalah sebagai berikut:
- analisis statis
- kompilasi
- tes unit
- tes integrasi
- Tes UI
- cek manual
Perubahan yang ditolak pada tahap N konveyor tidak ditransfer ke tahap N +1.
Kenapa begitu, dan bukan sebaliknya? Di bagian pengujian pipa, penguji mengenali piramida pengujian yang terkenal.
Tes piramida. Sumber: artikel Martin Fowler.Di bagian bawah piramida ini terdapat tes yang lebih mudah ditulis, yang lebih cepat dieksekusi dan tidak memiliki kecenderungan positif palsu. Karena itu, harus ada lebih banyak, mereka harus mencakup lebih banyak kode dan dieksekusi terlebih dahulu. Di bagian atas piramida, semuanya adalah sebaliknya, sehingga jumlah tes integrasi dan UI harus dikurangi ke minimum yang diperlukan. Orang dalam rantai ini adalah sumber daya yang paling mahal, lambat, dan tidak dapat diandalkan, sehingga ia berada di ujung dan melakukan pekerjaan hanya jika langkah sebelumnya tidak menunjukkan adanya cacat. Namun, sesuai dengan prinsip yang sama, konveyor dibangun di bagian yang tidak terkait langsung dengan pengujian!
Saya ingin menawarkan analogi dalam bentuk sistem penyaringan air multi-tahap. Air kotor (perubahan dengan cacat) disuplai ke pintu masuk, di pintu keluar kita harus mendapatkan air bersih, di mana semua polusi yang tidak diinginkan dihilangkan.
Filter bertingkat. Sumber: Wikimedia CommonsSeperti yang Anda ketahui, filter pembersih dirancang sehingga setiap kaskade berikutnya dapat menyaring sebagian kecil kontaminan. Pada saat yang sama, kaskade yang lebih kasar memiliki throughput yang lebih besar dan biaya yang lebih rendah. Dalam analogi kami, ini berarti bahwa gerbang kualitas masukan memiliki kecepatan yang lebih besar, membutuhkan lebih sedikit usaha untuk memulai dan mereka sendiri lebih bersahaja dalam pekerjaan mereka - dan dalam urutan inilah mereka dibangun. Peran analisis statis, yang, seperti yang sekarang kita pahami, mampu menyingkirkan hanya cacat paling kotor, adalah peran parut "perangkap tanah" di awal kaskade filter.
Analisis statis saja tidak meningkatkan kualitas produk akhir, sama seperti pengumpul lumpur tidak membuat air minum. Namun demikian, secara umum bersama dengan elemen konveyor lainnya, kepentingannya jelas. Meskipun dalam filter multistage, tahap-tahap output berpotensi mampu menangkap segala sesuatu yang sama dengan yang input, jelas apa konsekuensi upaya untuk melakukan dengan tahap-tahap halus tanpa tahap-tahap input akan mengarah ke sana.
Tujuan dari "pengumpul kotoran" adalah untuk membebaskan kaskade berikutnya dari menangkap cacat yang sangat kotor. Misalnya, setidaknya orang yang membuat tinjauan kode tidak boleh terganggu oleh kode yang diformat secara salah dan pelanggaran standar pengkodean yang telah ditetapkan (seperti tanda kurung tambahan atau cabang yang terlalu dalam). Bug seperti NPE harus ditangkap oleh unit test, tetapi jika bahkan sebelum pengujian, penganalisa memberi tahu kami bahwa bug itu pasti terjadi, ini akan secara signifikan mempercepat koreksi.
Saya percaya sekarang jelas mengapa analisis statis tidak meningkatkan kualitas produk, jika diterapkan secara sporadis, dan harus digunakan terus menerus untuk menyaring perubahan dengan cacat kotor. Pertanyaannya adalah apakah penggunaan analisa statis akan meningkatkan kualitas produk Anda, kira-kira setara dengan pertanyaan "apakah kualitas air minum yang diambil dari reservoir yang kotor akan meningkat jika dilewatkan melalui saringan?"
Implementasi dalam proyek warisan
Pertanyaan praktis yang penting: bagaimana mengintegrasikan analisis statis ke dalam proses integrasi berkesinambungan sebagai "gerbang kualitas"? Dalam hal tes otomatis, semuanya jelas: ada serangkaian tes, jatuhnya salah satu dari mereka adalah alasan yang cukup untuk percaya bahwa perakitan tidak melewati gerbang kualitas. Upaya menetapkan gerbang dengan cara yang sama berdasarkan hasil analisis statis gagal: ada terlalu banyak peringatan analisis pada kode lawas, Anda tidak ingin sepenuhnya mengabaikannya, tetapi tidak mungkin menghentikan pengiriman produk hanya karena mengandung peringatan penganalisa.
Ketika diterapkan untuk pertama kalinya, alat analisis menghasilkan sejumlah besar peringatan pada proyek apa pun, yang sebagian besar tidak terkait dengan berfungsinya produk. Tidak mungkin untuk mengoreksi semua pernyataan ini sekaligus, dan banyak yang tidak perlu. Pada akhirnya, kita tahu bahwa produk kita secara keseluruhan berfungsi, dan sebelum diperkenalkannya analisis statis!
Akibatnya, banyak yang terbatas pada penggunaan analisis statis episodik, atau menggunakannya hanya dalam mode informasi, ketika laporan analisis hanya dikeluarkan selama perakitan. Ini setara dengan tidak adanya analisis, karena jika kita sudah memiliki banyak peringatan, maka kemunculan yang lain (semena-mena serius) ketika mengubah kode tidak diperhatikan.
Metode pemberian gerbang kualitas berikut diketahui:
- Menetapkan batas jumlah total peringatan atau jumlah peringatan dibagi dengan jumlah baris kode. Ini bekerja buruk, karena gerbang seperti itu dengan bebas melewatkan perubahan dengan cacat baru sampai batas mereka terlampaui.
- Memperbaiki, pada saat tertentu, semua peringatan lama dalam kode sebagai diabaikan, dan menolak untuk membangun ketika peringatan baru terjadi. Fungsionalitas ini disediakan oleh PVS-studio dan beberapa sumber daya online, misalnya, Codacy. Saya tidak dapat bekerja di PVS-studio, karena pengalaman saya dengan Codacy, masalah utama mereka adalah menentukan apa yang "lama" dan apa yang "baru" adalah algoritma yang agak rumit dan tidak selalu bekerja, terutama jika file banyak dimodifikasi atau diganti namanya. Dalam ingatan saya, Codacy dapat melewati peringatan baru dalam permintaan tarik, dan pada saat yang sama tidak melewatkan permintaan tarik karena peringatan yang tidak terkait dengan perubahan dalam kode PR ini.
- Menurut pendapat saya, solusi paling efektif dijelaskan dalam buku Pengiriman Berkelanjutan "ratcheting" ("ratcheting"). Gagasan utamanya adalah bahwa properti dari setiap rilis adalah jumlah peringatan dari analisis statis, dan hanya perubahan yang diizinkan yang tidak menambah jumlah total peringatan.
Ratchet
Cara kerjanya seperti ini:
- Pada tahap awal, jumlah peringatan dalam kode yang ditemukan oleh analis dicatat dalam metadata tentang rilis. Jadi, ketika membangun cabang utama, tidak hanya "rilis 7.0.2", tetapi "rilis 7.0.2, yang berisi 100.500 Checkstyle peringatan" ditulis ke manajer repositori Anda. Jika Anda menggunakan manajer repositori tingkat lanjut (seperti Artifactory), menyimpan metadata tersebut tentang rilis Anda mudah.
- Sekarang, setiap permintaan penarikan selama pertemuan membandingkan jumlah peringatan yang diterima dengan nomor dalam rilis saat ini. Jika PR mengarah ke peningkatan angka ini, maka kode tidak lulus gerbang kualitas untuk analisis statis. Jika jumlah peringatan berkurang atau tidak berubah, maka itu berlalu.
- Pada rilis berikutnya, jumlah peringatan yang dihitung akan ditulis ulang ke metadata rilis.
Begitu sedikit demi sedikit, tetapi dengan mantap (seperti dengan ratchet), jumlah peringatan akan cenderung nol. Tentu saja, sistem bisa dibodohi dengan memperkenalkan peringatan baru, tetapi mengoreksi orang lain. Ini normal, karena dalam jarak yang jauh ia memberikan hasil: peringatan dikoreksi, sebagai suatu peraturan, tidak secara individu, tetapi segera oleh sekelompok jenis tertentu, dan semua peringatan yang mudah dihilangkan dengan cepat dihilangkan.
Grafik ini menunjukkan jumlah total peringatan Checkstyle selama setengah tahun bekerja seperti ratchet di
salah satu proyek OpenSource kami . Jumlah peringatan telah berkurang dengan urutan besarnya, dan ini terjadi secara alami, sejalan dengan pengembangan produk!

Saya menggunakan versi modifikasi dari metode ini, secara terpisah menghitung peringatan yang dipisahkan oleh modul proyek dan alat analisis, file YAML yang dihasilkan dengan metadata rakitan terlihat seperti ini:
celesta-sql: checkstyle: 434 spotbugs: 45 celesta-core: checkstyle: 206 spotbugs: 13 celesta-maven-plugin: checkstyle: 19 spotbugs: 0 celesta-unit: checkstyle: 0 spotbugs: 0
Dalam sistem CI canggih apa pun, "ratchet" dapat diterapkan untuk semua alat analisis statis, tanpa bergantung pada plugin dan alat pihak ketiga. Setiap analisis menghasilkan laporannya dalam bentuk teks atau XML sederhana, yang mudah dianalisis. Tetap hanya mendaftarkan logika yang diperlukan dalam skrip CI. Anda dapat melihat bagaimana ini diterapkan dalam proyek sumber terbuka kami berdasarkan Jenkins dan Artifactory di
sini atau di
sini . Kedua contoh bergantung pada pustaka
ratchetlib : metode
countWarnings()
menghitung tag xml dalam file yang dihasilkan oleh Checkstyle dan Spotbugs, dan
compareWarningMaps()
mengimplementasikan ratchet yang sama, melempar kesalahan ketika jumlah peringatan di salah satu kategori meningkat.
Implementasi ratchet yang menarik dimungkinkan untuk menganalisis ejaan komentar, literal teks, dan dokumentasi menggunakan aspell. Seperti yang Anda ketahui, saat memeriksa ejaan, tidak semua kata yang tidak dikenal ke kamus standar salah, mereka dapat ditambahkan ke kamus pengguna. Jika Anda membuat kamus khusus bagian dari kode sumber proyek, maka gerbang kualitas untuk ejaan dapat dirumuskan sebagai berikut: menjalankan aspell dengan kamus standar dan kustom
tidak akan menemukan kesalahan ejaan apa pun.
Tentang pentingnya memperbaiki versi penganalisis
Kesimpulannya, hal-hal berikut harus diperhatikan: tidak peduli bagaimana Anda mengintegrasikan analisis ke dalam pipa pengiriman Anda, versi penganalisa harus diperbaiki. Jika Anda mengizinkan penganalisis untuk memperbarui secara spontan, maka ketika merakit permintaan tarikan berikutnya, cacat baru mungkin "muncul" yang tidak terkait dengan mengubah kode, tetapi terhubung dengan fakta bahwa penganalisis baru hanya dapat menemukan lebih banyak cacat - dan ini akan merusak proses menerima permintaan tarikan . Pembaruan penganalisis harus merupakan tindakan sadar. Namun, memperbaiki secara ketat versi setiap komponen majelis umumnya merupakan persyaratan yang diperlukan dan topik untuk percakapan terpisah.
Kesimpulan
- Analisis statis tidak akan menemukan Anda bug dan tidak akan meningkatkan kualitas produk Anda sebagai akibat dari satu aplikasi. Efek positif pada kualitas hanya disediakan oleh penggunaannya yang konstan dalam proses pengiriman.
- Mencari bug sama sekali bukan tugas analisis utama, sebagian besar fungsi bermanfaat tersedia di perangkat opensource.
- Menerapkan gerbang mutu berdasarkan hasil analisis statis pada tahap pertama dari pipa pengiriman, menggunakan ratchet untuk kode lawas.
Referensi
- Pengiriman terus menerus
- A. Kudryavtsev: Analisis program: bagaimana memahami bahwa Anda adalah laporan programmer yang baik tentang berbagai metode analisis kode (tidak hanya statis!)