Bug paling murah adalah yang ditemukan sebelum kode pernah berjalan; bug paling mahal adalah yang ditemukan oleh pengguna di produksi. Di antara keduanya ada unit test, code review, dan satu teknik yang bekerja bahkan sebelum program dieksekusi: static analysis. Setelah sepanjang section ini kita menulis type declaration dan anotasi PHPDoc, kini saatnya melihat alat yang benar-benar menagih kontrak-kontrak tersebut. Pada Chapter ini kita akan memahami cara kerja static analysis, jenis bug yang bisa ditemukannya (dengan contoh nyata), batasnya, serta peta alat PHPStan, Psalm, dan IDE.

Apa Itu Static Analysis #

Static analysis adalah pemeriksaan kode sumber tanpa menjalankannya. Sebuah static analyzer membaca seluruh berkas proyek, membangun model simbol (class, function, variabel beserta tipenya) dari type declaration native dan anotasi PHPDoc, lalu menelusuri setiap cabang if, match, dan loop untuk menyimpulkan tipe tiap ekspresi di titik mana pun. Ketika ekspresi bertentangan dengan kontrak — memanggil method yang tidak ada, mengirim string ke parameter int — analyzer melaporkannya sebagai error.

Perbedaan mendasar dengan unit test terletak pada cara menempuh jalur eksekusi. Test membuktikan jalur yang diuji benar untuk data uji yang diberikan; jalur dan data yang tidak diuji tetap gelap. Analyzer tidak butuh data sama sekali — analyzer menelusuri semua jalur secara simbolis, sehingga cabang yang jarang tersentuh pun diperiksa. Keduanya saling melengkapi, bukan saling menggantikan.

Kabar baiknya: seluruh bekal section ini langsung terpakai. Type declaration adalah bahan bakar utama analyzer, dan PHPDoc dari Chapter sebelumnya mengisi celah yang native tidak mampu nyatakan. Semakin kaya informasi tipe, semakin tajam kesimpulan analyzer.

Jenis Bug yang Ditemukan Sebelum Kode Berjalan #

Empat keluarga bug berikut adalah makanan pokok static analyzer. Setiap contoh di bawah ini adalah PHP yang benar-benar dapat dijalankan — kita sengaja memicu kegagalannya di runtime untuk melihat apa yang sebenarnya dicegah oleh analyzer.

Method yang Tidak Ada #

Salah ketik nama method adalah bug klasik yang baru muncul saat baris tersebut tereksekusi:

php
<?php

class Pelanggan
{
    public function namaLengkap(): string
    {
        return 'Budi Santoso';
    }
}

$pelanggan = new Pelanggan();

try {
    echo $pelanggan->namaDepan(), "\n";
} catch (Error $e) {
    echo 'Tertangkap di runtime: ', $e->getMessage(), "\n";
}

Output yang Dihasilkan:

text
Tertangkap di runtime: Call to undefined method Pelanggan::namaDepan()

Pemanggilan $pelanggan->namaDepan() melempar Error fatal karena class Pelanggan hanya memiliki namaLengkap(). Kita menangkapnya dengan try-catch agar program tetap hidup untuk menunjukkan pesannya. PHPStan dan Psalm mendeteksi kasus ini dalam hitungan detik tanpa menjalankan satu baris pun, karena keduanya memiliki daftar lengkap method milik Pelanggan dari hasil pembacaan class.

Nilai Kembali yang Salah Tipe #

Kontradiksi antara type declaration dan isi function terdeteksi runtime sebagai TypeError:

php
<?php

declare(strict_types=1);

function ambilNamaPembeli(): string
{
    return 42;
}

try {
    echo ambilNamaPembeli(), "\n";
} catch (TypeError $e) {
    echo 'Tertangkap di runtime: ', $e->getMessage(), "\n";
}

Output yang Dihasilkan:

text
Tertangkap di runtime: ambilNamaPembeli(): Return value must be of type string, int returned

Function ambilNamaPembeli() berjanji mengembalikan string, tetapi baris return 42 mengirim int. Pada mode strict_types, interpreter menolaknya begitu return dieksekusi. Analyzer tidak perlu menunggu pemanggilan pertama — kecocokan antara : string dan ekspresi 42 sudah bisa dinilai langsung dari kode sumber.

Variabel yang Salah Ketik #

Tidak semua bug berakhir fatal. Salah ketik variabel hanya memicu Warning dan nilai null:

php
<?php

set_error_handler(
    function (int $level, string $pesan): bool {
        echo "Peringatan tertangkap: $pesan\n";

        return true;
    }
);

$namaPengguna = 'Siti';
echo $namaPenggun;

Output yang Dihasilkan:

text
Peringatan tertangkap: Undefined variable $namaPenggun

Baris terakhir membaca $namaPenggun — variabel yang tidak pernah didefinisikan karena huruf a tertinggal. PHP 8 menimbalkan Warning, nilai yang dibaca adalah null, dan echo mencetak string kosong. Karena tidak ada yang crash, bug semacam ini bisa lolos bertahun-tahun. Static analyzer menandainya sejak berkas disimpan: variabel $namaPenggun tidak pernah ada di scope tersebut.

Kode yang Tidak Pernah Dijalankan #

Kode mati (dead code) sering menjadi sisa refactor yang terlupakan:

php
<?php

function labelStatus(string $status): string
{
    return match ($status) {
        'aktif' => 'AKTIF',
        'nonaktif' => 'NONAKTIF',
    };

    echo 'Baris ini tidak pernah dijalankan';
}

echo labelStatus('aktif'), "\n";

Output yang Dihasilkan:

text
AKTIF

Pernyataan echo setelah return tidak akan pernah tereksekusi — return mengakhiri function sebelum sempat sampai ke sana. Program tetap berjalan normal dan mencetak AKTIF, sehingga kode mati tidak terasa mengganggu, padahal bisa menyembunyikan niat yang tidak terealisasi (misalnya logika yang seharusnya dijalankan sebelum return). Analyzer melaporkan kode tak terjangkau sebagai gejala kesalahan logika penempatan.

Yang Tidak Bisa Ditemukan: Bug Logika #

Static analysis bekerja dengan kontrak tipe, bukan dengan maksud bisnis. Perhatikan function berikut:

php
<?php

declare(strict_types=1);

function hitungTotalHarga(int $hargaSatuan, int $jumlah): int
{
    return $hargaSatuan - $jumlah; // bug logika: seharusnya perkalian
}

echo hitungTotalHarga(5000, 3), "\n";

Output yang Dihasilkan:

text
4997

Semua kontrak terpenuhi: parameter int, kembali int, tidak ada variabel tak didefinisikan, tidak ada method hantu. PHPStan dan Psalm akan diam seribu bahasa — dan memang seharusnya, karena 5000 - 3 = 4997 secara tipe tidak salah, secara bisnis jelas keliru (seharusnya 5000 * 3). Analyzer tidak tahu bahwa maksud function adalah mengalikan; maksud hanya bisa diuji lewat spesifikasi eksekusi, itulah wilayah unit test. Aturan praktisnya: static analysis membuktikan kode konsisten dengan tipenya, unit test membuktikan kode benar untuk maksudnya.

Sound dan Unsound #

Dalam literatur, sebuah analyzer disebut sound jika tidak pernah menyatakan kode aman ketika ada kemungkinan (sekecil apa pun) kode tersebut salah. PHP membuat soundness penuh sangat sulit: mixed dari json_decode(), eval(), variable variables, dan properti dinamis memungkinkan bentuk kode yang mustahil dipetakan secara statis. Karena itu PHPStan dan Psalm memilih jalur pragmatis: ketika tipe tidak bisa diketahui, nilai diberi tipe mixed dan diperlakukan sebagai wilayah abu-abu — analyzer tidak menebak-nebak, sehingga false positive tetap rendah.

Konsekuensinya, analyzer PHP bersifat unsound by design: ada bug yang bisa lolos (false negative), terutama ketika data mixed mengalir tanpa diperiksa. Justru karena itu level analisis yang ketat terhadap mixed — yang akan kita bahas pada Chapter Baseline dan Analysis Levels — begitu penting: semakin sedikit mixed yang lolos, semakin dekat kita ke jaminan kebenaran tipe.

PHPStan, Psalm, dan IDE #

Tiga kategori alat membaca kontrak tipe di ekosistem PHP, masing-masing dengan wilayah kerja berbeda:

AspekPHPStanPsalmIDE (PhpStorm, VS Code)
Bentukalat CLI + CIalat CLI + CItertanam di editor
Momen pemeriksaansaat commit / buildsaat commit / buildsaat mengetik
Cakupanseluruh proyek, lintas berkasseluruh proyek, lintas berkasberkas aktif + referensi
Fitur khaslevel 0-10, baseline, ekstensitaint analysis keamanan, --alterautocompletion, refactor cepat
Umpan balikdetik hingga menitdetik hingga menitinstan

PHPStan dan Psalm adalah static analyzer penuh: keduanya membangun model seluruh proyek dan cocok dijalankan di CI. Psalm dibesarkan di Vimeo dengan kekuatan tambahan berupa taint analysis (menelusuri aliran data tak tepercaya hingga ke lubang keamanan) dan fitur --alter yang memperbaiki kode otomatis. IDE memberikan pemeriksaan yang lebih dangkal tetapi instan — kesalahan diketahui begitu baris selesai diketik — dan sebagian besar IDE modern memakai mesin penalaran tipe yang serupa. Kombinasi ideal dalam praktik: IDE saat menulis, analyzer di CI sebelum merge.

Adopsi Bertahap dan Biayanya #

Memasang analyzer pada proyek lama yang minim tipe sering menghasilkan ratusan bahkan ribuan temuan sekaligus. Strategi adopsi yang lazim mengikuti tangga berikut:

  1. Mulai dari level analisis paling longgar (PHPStan level 0) supaya hanya bug fatal yang dilaporkan.
  2. Bekukan temuan lama dengan baseline agar tim bisa bekerja tanpa tenggelam dalam riwayat.
  3. Naikkan level satu tingkat setiap kali utang temuan selesai dilunasi.
  4. Pasang analyzer sebagai gerbang CI: pull request yang menambah temuan baru otomatis gagal.

Biayanya nyata: waktu perbaikan awal, diskusi soal false positive, dan disiplin menjaga anotasi tetap sinkron. Imbalannya juga nyata: bug kelas typo, tipe, dan method hantu — yang biasanya baru terlihat di produksi — berpindah ke detik-detik pertama penulisan kode. Dua Chapter berikutnya mempraktikkan kedua analyzer utama secara langsung, dimulai dari PHPStan .