NoVerify: linter untuk PHP dari Tim VKontakte sekarang berada di domain publik



Saya akan memberi tahu Anda bagaimana kami berhasil menulis linter yang ternyata cukup cepat untuk memeriksa perubahan selama setiap push git dan melakukannya dalam 5-10 detik dengan basis kode 5 juta baris dalam PHP. Kami menyebutnya NoVerify.

NoVerify mendukung hal-hal dasar seperti transisi ke definisi dan mencari kegunaan dan mampu bekerja dalam mode Server Bahasa . Pertama-tama, alat kami berfokus pada pencarian kesalahan potensial, tetapi juga dapat memeriksa gaya. Hari ini, kode sumbernya muncul di open-source di GitHub. Cari tautannya di akhir artikel.

Mengapa kita membutuhkan linter kita


Pada pertengahan 2018, kami memutuskan sudah waktunya untuk mengimplementasikan linter untuk kode PHP. Ada dua tujuan: untuk mengurangi jumlah kesalahan yang dilihat pengguna, dan lebih ketat memantau kepatuhan dengan gaya kode. Penekanan utama adalah pada pencegahan kesalahan khas: keberadaan variabel yang tidak dideklarasikan dan tidak terpakai dalam kode, kode yang tidak terjangkau, dan lainnya. Saya juga ingin penganalisa statis bekerja secepat mungkin pada basis kode kami (5-6 juta baris kode PHP pada saat penulisan).

Seperti yang mungkin Anda ketahui, kode sumber untuk sebagian besar situs ditulis dalam PHP dan dikompilasi menggunakan KPHP , jadi akan logis untuk menambahkan cek ini ke kompiler. Namun pada kenyataannya, tidak semua kode masuk akal untuk dijalankan melalui KPHP - misalnya, kompiler tidak kompatibel dengan perpustakaan pihak ketiga, jadi untuk beberapa bagian situs PHP biasa masih digunakan. Mereka juga penting dan harus diperiksa oleh linter, jadi, sayangnya, tidak ada cara untuk mengintegrasikannya ke KPHP.

Mengapa NoVerify


Mengingat jumlah kode PHP (saya akan mengingatkan Anda bahwa ini adalah 5-6 juta baris), tidak mungkin untuk "memperbaikinya" segera sehingga melewati pemeriksaan kami di linter. Namun demikian, saya ingin kode perubahan secara bertahap menjadi lebih bersih dan lebih ketat mengikuti standar pengkodean, dan juga mengandung lebih sedikit kesalahan. Oleh karena itu, kami memutuskan bahwa linter harus dapat memeriksa perubahan yang akan diluncurkan oleh pengembang, dan tidak bersumpah pada yang lain.

Untuk melakukan ini, linter perlu mengindeks seluruh proyek, menganalisis file sepenuhnya sebelum dan setelah perubahan, dan menghitung perbedaan antara peringatan yang dihasilkan. Peringatan baru ditampilkan kepada pengembang, dan kami meminta mereka untuk memperbaikinya sebelum push dapat dilakukan.

Tetapi ada situasi ketika perilaku ini tidak diinginkan, dan kemudian pengembang dapat mendorong tanpa kait lokal - menggunakan git push --no-verify . Opsi --no-verify dan memberi nama ke linter :)

Apa alternatifnya


Basis kode dalam VK menggunakan OOP kecil dan pada dasarnya terdiri dari fungsi dan kelas dengan metode statis. Jika kelas dalam PHP mendukung pengisian otomatis, maka fungsi tidak. Oleh karena itu, kami tidak dapat menggunakan analisis statis tanpa modifikasi signifikan, yang mendasarkan pekerjaan mereka pada fakta bahwa pengisian otomatis akan memuat semua kode yang hilang. Linter seperti itu, misalnya, mazmur dari Vimeo .

Kami memeriksa alat analisis statis berikut:

  • PHPStan - single-threaded, membutuhkan pengisian otomatis, analisis basis kode telah mencapai 30% dalam setengah jam;
  • Phan - bahkan dalam mode cepat dengan 20 proses, analisis terhenti sebesar 5% setelah 20 menit;
  • Mazmur - membutuhkan pengisian otomatis, analisis membutuhkan waktu 10 menit (saya masih ingin menjadi lebih cepat);
  • PHPCS - memeriksa gayanya, tetapi bukan logika;
  • phpcf - hanya memeriksa untuk pemformatan.

Seperti yang dapat Anda tebak dari judul artikel, tidak ada alat ini yang memenuhi persyaratan kami, jadi kami menulis sendiri.

Bagaimana prototipe dibuat?


Pertama, kami memutuskan untuk membuat prototipe kecil untuk memahami apakah perlu mencoba membuat linter yang lengkap. Karena salah satu persyaratan penting untuk linter adalah kecepatannya, bukan PHP yang kami pilih Go. "Cepat" adalah untuk memberikan umpan balik kepada pengembang secepat mungkin, sebaiknya dalam waktu tidak lebih dari 10-20 detik. Kalau tidak, siklus "koreksi kode, jalankan linter lagi" mulai secara signifikan memperlambat pengembangan dan merusak suasana hati orang :)

Karena Go dipilih untuk prototipe, Anda memerlukan parser PHP. Ada beberapa di antaranya, tetapi proyek php-parser tampaknya paling matang bagi kami. Parser ini tidak sempurna dan masih sedang dikembangkan, tetapi untuk tujuan kami cukup cocok.

Untuk prototipe, diputuskan untuk mencoba mengimplementasikan salah satu yang paling sederhana, sekilas, inspeksi: akses ke variabel yang tidak ditentukan.

Gagasan dasar untuk mengimplementasikan inspeksi semacam itu terlihat sederhana: untuk setiap cabang (misalnya, untuk jika), buat cakupan bersarang terpisah dan gabungkan jenis variabel saat keluar dari sana. Contoh:

 <?php if (rand()) { $a = 42; //  : { $a: int } } else { $b = "test"; $a = "another_test"; //  : { $b: string, $a: string } } //   : { $b: string?, $a: int|string } echo $a, $b; //       , //   $b    

Itu terlihat sederhana, bukan? Dalam kasus pernyataan bersyarat biasa, semuanya bekerja dengan baik. Tetapi kita harus menangani, misalnya, beralih tanpa putus;

 <?php switch (rand()) { case 1: $a = 1; // { $a: int } case 2: $b = 2; // { $a: int, $b: int } default: $c = 3; // { $a: int, $b: int, $c: int } } // { $a: int?, $b: int?, $c: int } 

Tidak segera jelas dari kode bahwa $ c akan selalu didefinisikan. Secara khusus, contoh ini fiktif, tetapi menggambarkan dengan baik apa saat-saat sulit bagi orang yang suka (dan untuk orang dalam kasus ini juga).

Pertimbangkan contoh yang lebih kompleks:

 <?php exec("hostname", $out, $retval); echo $out, $retval; // { $out: ???, $retval: ??? } 

Tanpa mengetahui tanda tangan dari fungsi exec, tidak dapat dikatakan apakah $ out dan $ retval akan ditentukan. Tanda tangan dari fungsi-fungsi bawaan dapat diambil dari repositori github.com/JetBrains/phpstorm-stubs . Tetapi masalah yang sama akan terjadi ketika memanggil fungsi yang ditentukan pengguna, dan tanda tangannya hanya dapat ditemukan dengan mengindeks seluruh proyek. Fungsi exec mengambil argumen kedua dan ketiga dengan referensi, yang berarti variabel $ out dan $ retval dapat didefinisikan. Di sini, mengakses variabel-variabel ini tidak selalu merupakan kesalahan, dan linter tidak boleh bersumpah pada kode tersebut.

Masalah serupa dengan passing link implisit muncul dengan metode, tetapi pada saat yang sama, kebutuhan untuk menyimpulkan tipe variabel ditambahkan:

 <?php if (rand()) { $a = some_func(); } else { $a = other_func(); } $a->some_method($b); echo $b; 

Kita perlu mengetahui tipe apa yang mengembalikan fungsi some_func () dan other_func () untuk kemudian menemukan metode yang disebut some_method di kelas-kelas ini. Hanya dengan begitu kita dapat mengatakan apakah variabel $ b akan didefinisikan atau tidak. Situasi ini diperumit oleh fakta bahwa seringkali fungsi dan metode sederhana tidak memiliki anotasi phpdoc, jadi Anda masih harus dapat menghitung jenis fungsi dan metode berdasarkan implementasinya.

Ketika mengembangkan prototipe, saya harus menerapkan sekitar setengah dari semua fungsi sehingga pemeriksaan paling sederhana bekerja sebagaimana mestinya.

Bekerja sebagai server bahasa


Untuk membuatnya lebih mudah untuk men-debug logika linter dan lebih mudah untuk melihat peringatan yang dikeluarkannya, kami memutuskan untuk menambahkan mode operasi sebagai server bahasa untuk PHP . Dalam mode integrasi dengan Visual Studio Code, tampilannya seperti ini:



Dalam mode ini, akan lebih mudah untuk menguji hipotesis dan menguji kasus yang rumit (setelah itu Anda perlu menulis tes, tentu saja). Juga bagus untuk menguji kinerja: bahkan pada file besar php-parser on Go menunjukkan kecepatan yang baik.

Dukungan server bahasa jauh dari ideal, karena tujuan utamanya adalah untuk men-debug aturan linter. Namun, dalam mode ini ada beberapa fitur tambahan:

  1. Kiat untuk nama variabel, konstanta, fungsi, properti, dan metode.
  2. Sorot jenis-jenis variabel turunan.
  3. Pergi ke definisi.
  4. Cari kegunaan.

Inferensi tipe "malas"


Dalam mode server bahasa, yang berikut ini diperlukan untuk bekerja: Anda mengubah kode dalam satu file, dan kemudian, ketika Anda beralih ke yang lain, Anda harus bekerja dengan informasi yang sudah diperbarui tentang jenis yang dikembalikan dalam fungsi atau metode. Bayangkan file yang sedang diedit dalam urutan berikut:

 <?php //  A.php,  1 class A { /** @var int */ public $prop; } //  B.php,   class B { public static function something() { $obj = new A; return $obj->prop; } } //  C.php,   $c = B::something(); // $c   int //  A.php,  2 class A { /** @var string <---   string */ public $prop; } //  C.php,   $c = B::something(); // $c   string,   B.php,  C.php   

Mengingat bahwa kami tidak memaksa pengembang untuk selalu menulis PHPDoc (terutama dalam kasus sederhana seperti itu), kami memerlukan cara untuk menyimpan informasi tentang apa yang mengembalikan fungsi B :: something (). Sehingga ketika file A.php berubah, ketikkan informasi dalam file C.php segera.

Salah satu solusi yang mungkin adalah dengan menyimpan "tipe malas." Misalnya, tipe kembalinya dari metode B :: something () sebenarnya adalah tipe ekspresi (new A) -> prop. Dalam formulir ini, linter menyimpan informasi tentang jenisnya, dan berkat ini, Anda dapat menyimpan semua informasi meta untuk setiap file dan memperbaruinya hanya ketika file ini berubah. Ini harus dilakukan dengan hati-hati agar informasi yang terlalu spesifik tentang jenis tidak bocor. Juga perlu untuk mengubah versi cache ketika tipe logika inferensi berubah. Namun demikian, cache semacam itu mempercepat fase pengindeksan (yang akan saya bahas nanti) 5-10 kali dibandingkan dengan penguraian berulang semua file.

Dua fase kerja: pengindeksan dan analisis


Seperti yang kita ingat, bahkan untuk analisis kode paling sederhana, informasi diperlukan setidaknya tentang semua fungsi dan metode dalam proyek. Ini berarti Anda tidak dapat menganalisis hanya satu file secara terpisah dari proyek. Namun - bahwa ini tidak dapat dilakukan dalam satu pass: misalnya, PHP memungkinkan Anda untuk mengakses fungsi yang dinyatakan lebih lanjut dalam file.

Karena keterbatasan ini, operasi linter terdiri dari dua fase: pengindeksan primer dan analisis selanjutnya dari hanya file yang diperlukan. Sekarang, lebih lanjut tentang dua fase ini.

Fase pengindeksan


Pada fase ini, semua file diurai dan analisis lokal dari kode metode dan fungsi, serta kode di tingkat atas dilakukan (misalnya, untuk menentukan jenis variabel global). Informasi tentang variabel global yang dinyatakan, konstanta, fungsi, kelas dan metode mereka dikumpulkan dan ditulis ke cache. Untuk setiap file dalam proyek, cache adalah file terpisah pada disk.

Kamus global dari semua meta-informasi tentang proyek, yang tidak berubah di masa depan, * dikompilasi dari setiap bagian.

* Selain mode operasi sebagai server bahasa, saat pengindeksan dan analisis file yang diubah dilakukan untuk setiap pengeditan.

Fase analisis


Pada fase ini, kita dapat menggunakan meta-informasi (tentang fungsi, kelas ...) dan sudah langsung menganalisis kode. Berikut adalah daftar yang dapat diperiksa NoVerify secara default:

  • kode yang tidak terjangkau;
  • akses ke objek sebagai array;
  • jumlah argumen saat memanggil fungsi tidak cukup;
  • memanggil metode / fungsi yang tidak ditentukan;
  • akses ke properti kelas yang hilang / konstan;
  • kurangnya kelas;
  • PHPDoc tidak valid
  • akses ke variabel yang tidak ditentukan;
  • akses ke variabel yang tidak selalu ditentukan;
  • kurangnya "istirahat;" setelah kasus dalam switch / konstruksi kasus;
  • kesalahan sintaksis
  • variabel yang tidak digunakan.

Daftarnya cukup pendek, tetapi Anda dapat menambahkan cek khusus untuk proyek Anda.

Selama operasi linter, ternyata pemeriksaan yang paling berguna hanyalah yang terakhir (variabel yang tidak digunakan). Ini sering terjadi ketika Anda memperbaiki kode (atau menulis yang baru) dan menyegelnya dalam nama variabel: kode ini valid dari sudut pandang PHP, tetapi salah dalam logika.

Kecepatan kerja


Berapa lama perubahan yang ingin kami dorong diperiksa? Itu semua tergantung pada jumlah file. Dengan NoVerify, prosesnya bisa memakan waktu hingga satu menit (ini terjadi ketika saya mengubah 1400 file dalam repositori), tetapi jika ada beberapa pengeditan, maka biasanya semua pemeriksaan lulus dalam 4-5 detik. Selama ini, proyek ini sepenuhnya diindeks, mem-parsing file baru, serta analisisnya. Kami cukup mampu membuat linter untuk PHP, yang bekerja dengan cepat bahkan dengan basis kode besar kami.

Apa hasilnya?


Karena solusinya ditulis dalam Go, maka diperlukan untuk menggunakan repositori github.com/JetBrains/phpstorm-stubs agar memiliki definisi semua fungsi dan kelas yang dibangun dalam PHP. Sebagai imbalannya, kami mendapat kecepatan kerja yang tinggi (mengindeks 1 juta garis per detik, analisis 100 ribu garis per detik) dan mampu menambahkan cek dengan linter sebagai salah satu langkah pertama dalam kait kait git.

Basis yang nyaman dikembangkan untuk membuat inspeksi baru dan tingkat pemahaman kode yang dekat dengan PHPStorm tercapai. Karena kenyataan bahwa di luar kotak mode dengan perhitungan berbeda didukung, dimungkinkan untuk secara bertahap meningkatkan kode, menghindari konstruksi baru yang berpotensi bermasalah dalam kode baru.

Menghitung diff tidak ideal: misalnya, jika satu file besar dibagi menjadi beberapa yang kecil, maka git, dan karena itu NoVerify, tidak akan dapat menentukan bahwa kode tersebut dipindahkan, dan linter akan perlu memperbaiki semua masalah yang ditemukan. Dalam hal ini, perhitungan diff mencegah refactoring skala besar, sehingga dalam kasus seperti itu sering dinonaktifkan.

Menulis linter on Go memiliki keunggulan lain: tidak hanya AST parser lebih cepat dan mengkonsumsi lebih sedikit memori daripada PHP, tetapi analisis selanjutnya juga sangat cepat dibandingkan dengan apa pun yang dapat dilakukan dalam PHP. Ini berarti bahwa linter kami dapat melakukan analisis kode yang lebih kompleks dan lebih dalam, sambil mempertahankan kinerja tinggi (misalnya, fitur "tipe malas" memerlukan sejumlah besar perhitungan dalam proses).

Sumber terbuka


NoVerify tersedia di open source di GitHub

Nikmati penggunaan Anda dalam proyek Anda!

UPD: Saya menyiapkan demo yang berfungsi melalui WebAssembly . Satu-satunya batasan demo ini adalah tidak adanya definisi fungsi dari phpstorm-stubs, sehingga linter akan bersumpah pada fungsi bawaan.

Yuri Nasretdinov, pengembang departemen infrastruktur VKontakte

Source: https://habr.com/ru/post/id442284/


All Articles