Compiler javac sudah mengingatkan kita tentang tipe yang tidak cocok sebelum program berjalan — dan itu, sebenarnya, bentuk analisis statik yang paling akrab. Static analysis memperluas gagasan yang sama: memeriksa kode sumber (atau bytecode) untuk menemukan pola bermasalah tanpa menjalankan program. Hasilnya diperoleh dalam hitungan detik, mencakup seluruh jalur eksekusi yang mungkin — bukan hanya jalur yang kebetulan dilalui test — dan berjalan otomatis pada setiap build. Pada Chapter ini kita akan memahami apa yang diperiksa analisis statik, tiga kelas temuannya, batas kemampuannya, lalu membangun pemindai mini untuk melihat bahwa intinya dapat ditulis tangan.
Memeriksa Tanpa Menjalankan #
Perbedaan mendasar dengan test perlu dipahami lebih dulu. Unit test menjalankan kode dengan masukan tertentu lalu membandingkan hasilnya — kekuatannya adalah bukti perilaku nyata, kelemahannya adalah cakupan: jalur yang tidak diuji tetap gelap. Analisis statik membaca seluruh kode sebagai teks atau struktur (AST, bytecode) dan mencocokkannya dengan kumpulan aturan — kekuatannya adalah cakupan penuh, kelemahannya adalah tidak mengetahui nilai runtime: pemindai melihat if (x > 0) tanpa tahu apakah x pernah negatif.
Kedua pendekatan itu saling melengkapi, bukan bersaing. Test membuktikan perilaku, analisis statis menyapu pola kelalaian: variabel yang tidak terpakai, sumber daya yang tidak ditutup, perbandingan string dengan ==, dan ratusan pola sejenis yang tidak perlu menunggu runtime untuk diketahui.
Tiga Kelas Temuan #
Temuan alat analisis statik lazimnya terbagi dalam tiga kelas dengan tingkat urgensi berbeda:
| Kelas | Contoh temuan | Alat Java populer |
|---|---|---|
| Bug | == pada String, resource tak ditutup, equals tanpa hashCode | SpotBugs, Error Prone |
| Kerentanan keamanan | SQL dari rangkaian teks, kriptografi lemah | SpotBugs (plugin keamanan) |
| Gaya dan konvensi | penamaan, panjang baris, import terpakai | Checkstyle |
Urutan prioritas menalarinya pun berbeda: temuan bug dan kerentanan sebaiknya diperbaiki segera, sedangkan temuan gaya biasanya cukup dibereskan bertahap atau otomatis oleh formatter. Chapter terpisah membahas Checkstyle, SpotBugs, dan Error Prone satu per satu; di sini kita bangun intuisi mesinnya lebih dulu.
Pemindai Mini Buatan Sendiri #
Alat sekelas SpotBugs mengandalkan analisis aliran data, tetapi banyak aturan berguna sesungguhnya hanya pencocokan pola pada baris kode. Program berikut memeriksa cuplikan kode yang tersimpan sebagai string dengan empat aturan sederhana, lalu mencetak daftar temuannya:
import java.util.ArrayList;
import java.util.List;
public class PemindaiStatis {
// Cuplikan kode yang diperiksa; beku agar hasil pemeriksaan deterministik
static final String CUPLIKAN = """
import java.util.List;
import java.util.Date;
public class PelangganService {
public void SimpanData(int id) {
System.out.println("menyimpan " + id);
}
private boolean validasiEmail(String email) {
return email.contains("@");
}
}
""";
public static void main(String[] args) {
String[] baris = CUPLIKAN.split("\n", -1);
List<String> temuan = new ArrayList<>();
for (int n = 0; n < baris.length; n++) {
String b = baris[n].trim();
// Aturan 1: nama method seharusnya diawali huruf kecil
if (b.matches("(public|private|protected).*\\s([A-Z]\\w*)\\s*\\(.*")
&& !b.contains("class ")) {
temuan.add("baris " + (n + 1) + ": nama method diawali huruf kapital");
}
// Aturan 2: System.out.println tidak untuk kode produksi
if (b.contains("System.out.println")) {
temuan.add("baris " + (n + 1) + ": ganti System.out.println dengan logger");
}
// Aturan 3: baris melebihi 80 karakter
if (b.length() > 80) {
temuan.add("baris " + (n + 1) + ": panjang " + b.length() + " karakter (maks 80)");
}
}
// Aturan 4: import yang tidak pernah dipakai
for (String b : baris) {
if (b.startsWith("import ")) {
String kelas = b.substring(7, b.length() - 1);
String sederhana = kelas.substring(kelas.lastIndexOf('.') + 1);
if (hitungPemakaian(CUPLIKAN, sederhana) <= 1) {
temuan.add("import " + kelas + " tidak terpakai");
}
}
}
System.out.println("Jumlah temuan: " + temuan.size());
for (String t : temuan) {
System.out.println("- " + t);
}
}
static int hitungPemakaian(String teks, String kata) {
int jumlah = 0;
int pos = teks.indexOf(kata);
while (pos >= 0) {
jumlah++;
pos = teks.indexOf(kata, pos + kata.length());
}
return jumlah;
}
}Output yang Dihasilkan:
Jumlah temuan: 4
- baris 5: nama method diawali huruf kapital
- baris 6: ganti System.out.println dengan logger
- import java.util.List tidak terpakai
- import java.util.Date tidak terpakaiMari kita bedah cara kerja pemindai tersebut:
CUPLIKANsebagai string beku: kode yang diperiksa tidak dieksekusi — kita hanya membacanya sebagai teks, persis definisi analisis statik. Empat pelanggaran sengaja ditanam: nama method kapital,System.out.println, dan dua import tak terpakai.- Aturan 1 (
matchesdengan regex): mendeteksi deklarasi method yang namanya diawali huruf kapital —SimpanDatapada baris 5. Syarat!b.contains("class ")mencegah deklarasi class ikut tertangkap. - Aturan 2 dan 3: pencocokan substring dan penghitungan panjang baris — aturan paling murah namun tetap menangkap kelalaian nyata.
- Aturan 4 (
hitungPemakaian): menghitung kemunculan nama class dari setiapimport; jika hanya muncul satu kali (pada baris impornya sendiri), import itu tidak terpakai. Inilah deteksijava.util.Listdanjava.util.Datepada keluaran. - Laporan berupa daftar bernomor baris: format
baris N: ...meniru keluaran alat sungguhan sehingga pengembang dapat melompat langsung ke lokasi.
Batasan dan Temuan Palsu #
Dua jenis kegagalan melekat pada analisis statis. False negative: pemindai tidak menemukan masalah yang ada — misalnya, pemindai mini kita tidak memeriksa logika bisnis sama sekali. False positive: pemindai melaporkan sesuatu yang sebenarnya benar — misalnya aturan “tanpa System.out.println” akan menandai class utilitas CLI yang memang wajar mencetak ke layar.
Alat sungguhan mengatasinya dengan dua mekanisme: penandaan temuan berdasarkan keyakinan (confidence/rank), dan mekanisme pengecualian eksplisit — anotasi @SuppressWarning pada kode atau berkas konfigurasi suppression per aturan. Prinsipnya: pengecualian harus terlihat dan beralasan, bukan sekadar mematikan pemeriksaan karena temuan mengganggu.
Integrasi ke Pipeline CI #
Nilai analisis statik mencapai puncaknya ketika berjalan otomatis pada setiap perubahan. Pola integrasi umumnya: build menjalankan pemindai, dan pipeline gagal bila ada pelanggaran baru — bukan bila ada pelanggaran lama, sebab warisan kode lama tidak realistis dibereskan sekaligus. Konfigurasi plugin Maven berikut memperagakan pola gate tersebut:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<configLocation>checkstyle.xml</configLocation>
<failOnViolation>true</failOnViolation>
</configuration>
<executions>
<execution>
<phase>validate</phase>
<goals><goal>check</goal></goals>
</execution>
</executions>
</plugin>Elemen kuncinya failOnViolation: build gagal saat ada temuan, sehingga pelanggaran baru tidak dapat masuk ke branch utama tanpa disengaja. Banyak tim menambahkan laporan baseline — daftar temuan lama yang diizinkan — dan mengecilkan daftar itu seiring pembersihan kode.
Kita kini memahami kerangka besar analisis statik beserta batasnya. Tiga alat Java utama menempati sudut berbeda dari kerangka itu: Checkstyle menjaga gaya, SpotBugs menyapu bytecode mencari pola bug, dan Error Prone memeriksa saat kompilasi. Chapter berikutnya mulai dari yang paling dekat dengan pemindai mini kita — Checkstyle dan aturan-aturan gayanya.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
PemindaiStatis.java
Unduh berkasimport java.util.ArrayList;
import java.util.List;
public class PemindaiStatis {
// Cuplikan kode yang diperiksa; beku agar hasil pemeriksaan deterministik
static final String CUPLIKAN = """
import java.util.List;
import java.util.Date;
public class PelangganService {
public void SimpanData(int id) {
System.out.println("menyimpan " + id);
}
private boolean validasiEmail(String email) {
return email.contains("@");
}
}
""";
public static void main(String[] args) {
String[] baris = CUPLIKAN.split("\n", -1);
List<String> temuan = new ArrayList<>();
for (int n = 0; n < baris.length; n++) {
String b = baris[n].trim();
// Aturan 1: nama method seharusnya diawali huruf kecil
if (b.matches("(public|private|protected).*\\s([A-Z]\\w*)\\s*\\(.*")
&& !b.contains("class ")) {
temuan.add("baris " + (n + 1) + ": nama method diawali huruf kapital");
}
// Aturan 2: System.out.println tidak untuk kode produksi
if (b.contains("System.out.println")) {
temuan.add("baris " + (n + 1) + ": ganti System.out.println dengan logger");
}
// Aturan 3: baris melebihi 80 karakter
if (b.length() > 80) {
temuan.add("baris " + (n + 1) + ": panjang " + b.length() + " karakter (maks 80)");
}
}
// Aturan 4: import yang tidak pernah dipakai
for (String b : baris) {
if (b.startsWith("import ")) {
String kelas = b.substring(7, b.length() - 1);
String sederhana = kelas.substring(kelas.lastIndexOf('.') + 1);
if (hitungPemakaian(CUPLIKAN, sederhana) <= 1) {
temuan.add("import " + kelas + " tidak terpakai");
}
}
}
System.out.println("Jumlah temuan: " + temuan.size());
for (String t : temuan) {
System.out.println("- " + t);
}
}
static int hitungPemakaian(String teks, String kata) {
int jumlah = 0;
int pos = teks.indexOf(kata);
while (pos >= 0) {
jumlah++;
pos = teks.indexOf(kata, pos + kata.length());
}
return jumlah;
}
}