Setiap modul PHP yang saya tulis untuk situs direktori keanggotaan milik seorang klien melewati gerbang yang sama sebelum menyentuh server: lint lokal dengan php -l, pemeriksaan bahwa isinya ASCII murni, pemeriksaan fungsi yang dideklarasikan dua kali, lalu berkasnya ditulis ke /tmp di server, di-lint sekali lagi dengan php -l di sana, dan baru ditukar ke tempatnya lewat cat > supaya pemiliknya tetap www-data. Gerbang ini membosankan, dan justru itu gunanya. Suatu hari lint lokalnya menolak satu berkas modul dengan pesan yang kelihatannya sangat spesifik:
Unmatched '}' on line 466Saya buka baris 466. Isinya kurung kurawal penutup biasa. Saya hitung pasangan brace di sekitarnya, seimbang. Saya lipat blok demi blok dari atas ke bawah, masih seimbang. Tidak ada fungsi yang lupa ditutup, tidak ada if yang kelebihan penutup. Satu siklus kerja habis untuk memburu brace yang, belakangan terbukti, tidak pernah hilang.
Baris yang benar-benar bermasalah ada di sekitar baris 150, jauh dari angka yang ditunjuk pesan galat. Di sana ada satu komentar dokumentasi satu baris. Komentar itu menjelaskan bagaimana templat merender sebuah penanda, dan supaya jelas, saya tulis contoh markupnya apa adanya di dalam komentar. Bentuk minimalnya kira-kira begini:
<?php
function contoh_render_penanda( $mark ) {
// templat merendernya begini: "<?php echo $mark; ?> SEEN"
return $mark . ' SEEN';
}Bagi mata manusia, itu komentar. Bagi PHP, komentar itu berhenti tepat di ?>.
Kenapa ini terjadi
Tokenizer PHP memperlakukan ?> sebagai token keras, bahkan ketika dia muncul di tengah komentar. Komentar satu baris dengan // atau # hanya berjalan sampai ?> atau sampai akhir baris, mana yang lebih dulu ketemu. Ini bukan bug, ini perilaku yang memang didokumentasikan: tag tutup PHP selalu menang atas komentar satu baris.
Akibatnya, begitu tokenizer melewati ?> di dalam komentar tadi, mode PHP berakhir. Sisa baris itu dan semua yang mengikutinya menjadi keluaran HTML mentah, dan parser membaca sisa berkas sebagai teks. Brace yang seharusnya menutup fungsi di baris berikutnya ikut tertelan sebagai teks, hitungan pasangan brace bergeser satu, dan parser baru sempat mengeluh ketika bertemu } yang jauh di bawah. Itulah kenapa pesannya menunjuk baris 466 padahal pelakunya ada di sekitar baris 150. Angkanya jujur dari sudut pandang parser, tapi menyesatkan dari sudut pandang orang yang mencari brace.
Yang membuatnya sulit dilihat adalah karena kode di sekitarnya benar semua. Tidak ada kurung yang kurang, tidak ada titik koma yang lupa. Satu-satunya yang salah adalah dua karakter di dalam baris yang oleh editor saya diwarnai abu-abu sebagai komentar, dan warna abu-abu itu dengan sendirinya berkata "abaikan saya".
Perbaikannya
Perbaikan pada berkasnya sepele: contoh markup di dalam komentar diganti dengan kalimat biasa yang menggambarkan hasilnya, tanpa pernah menulis <?php maupun ?> di dalam komentar.
<?php
function contoh_render_penanda( $mark ) {
// templat menambahkan " SEEN" di belakang nilai yang di-echo
return $mark . ' SEEN';
}php -l langsung lolos, dan berkasnya naik ke server lewat gerbang yang biasa.
Yang lebih berharga adalah aturan diagnosisnya. Kalau php -l menunjuk unmatched } pada berkas yang brace-nya kelihatan seimbang, jangan mulai dari menghitung brace. Cari dulu ?> di dalam komentar:
grep -nE '(//|#).*\?>' inc/*.phpPola itu menangkap komentar // atau # yang di dalamnya masih ada tag tutup PHP, dan langsung menunjuk barisnya. Untuk kasus saya, satu perintah ini akan menghemat satu siklus penuh.
Aturan penulisannya saya pegang sejak itu: jangan pernah menulis ?> atau <?php … ?> di dalam komentar // atau #. Kalau perlu menjelaskan markup, gambarkan dalam prosa. Blok /* … */ boleh dipakai hanya kalau isinya juga tidak memuat ?>.
Ini bukan pertama kalinya komentar menjatuhkan lint di projek yang sama. Belum lama sebelumnya, di sesi pembangunan modul keanggotaan, ada urutan */ yang muncul di dalam komentar blok karena saya menulis pola nama seperti ini:
/* pola yang dilewati: *_extra_*/raw */Urutan */ di tengah pola itu diam-diam menutup komentarnya lebih awal, dan sisanya diparse sebagai kode, sehingga muncul parse error. php -l menangkapnya. Sejak kejadian itu, langkah lint di rencana kerja secara eksplisit berbunyi php -l, cek ASCII, dan awasi */. Kasus ?> ini tinggal menambah satu butir ke daftar yang sama.
Pelajaran
Komentar tidak sepenuhnya inert. Ada dua urutan karakter yang tetap dibaca PHP walaupun ada di dalam komentar: ?> di komentar satu baris, dan */ di komentar blok. Keduanya sama-sama tertangkap php -l, tapi pesan galatnya belum tentu menunjuk ke tempat yang benar, dan pesan unmatched } yang berjarak lebih dari tiga ratus baris dari sumbernya adalah contoh paling jelasnya.
Pelajaran ini sepupu dari aturan ASCII murni yang sudah lama saya pegang di sumber PHP: waspadai karakter literal di dalam berkas, bukan hanya logikanya. Kutip pintar, tag tutup, penutup komentar, semuanya satu keluarga. Kode yang kelihatan benar dan parser yang mengeluh di baris yang salah hampir selalu berarti ada karakter yang punya makna lebih dari yang saya kira.