Fitur pembunuh telah muncul di penganalisa statis NoVerify : cara deklaratif untuk menggambarkan inspeksi yang tidak memerlukan pemrograman Go dan kompilasi kode.
Untuk membuat Anda penasaran, saya akan menunjukkan kepada Anda deskripsi inspeksi yang sederhana namun bermanfaat:
$x && $x;
Pemeriksaan ini menemukan semua ekspresi logis &&
mana operan kiri dan kanan identik.
NoVerify adalah penganalisa statis untuk PHP yang ditulis dalam Go . Anda dapat membacanya di artikel “ NoVerify: Linter for PHP dari Tim VKontakte ”. Dan dalam ulasan ini saya akan berbicara tentang fungsionalitas baru dan bagaimana kami melakukannya.

Latar belakang
Ketika bahkan untuk pemeriksaan baru yang sederhana, Anda perlu menulis beberapa lusin baris kode di Go, Anda mulai bertanya-tanya: apakah mungkin sebaliknya?
On Go, kami telah menulis inferensi tipe, seluruh jalur pipa linter, cache metadata, dan banyak elemen penting lainnya yang tanpanya NoVerify tidak mungkin. Komponen-komponen ini unik, tetapi tugas-tugas seperti "melarang memanggil fungsi X dengan seperangkat argumen Y" tidak. Hanya untuk tugas-tugas sederhana seperti itu mekanisme aturan dinamis telah ditambahkan.
Aturan dinamis memungkinkan Anda untuk memisahkan internal yang kompleks dari penyelesaian masalah yang khas. File definisi dapat disimpan dan diversi secara terpisah - itu dapat diedit oleh orang-orang yang tidak terkait dengan pengembangan NoVerify itu sendiri. Setiap aturan menerapkan inspeksi kode (yang terkadang kami sebut verifikasi).
Ya, jika kami memiliki bahasa untuk menjelaskan aturan ini, Anda selalu dapat menulis templat yang semantik salah atau mengabaikan beberapa batasan jenis - dan ini mengarah pada false positive. Namun demikian, perlombaan data atau dereferencing nil
pointer melalui bahasa aturan tidak dimasukkan.
Deskripsi Template Bahasa
Bahasa deskripsi secara sintaksis kompatibel dengan PHP. Ini menyederhanakan studinya, dan juga memungkinkan untuk mengedit file aturan menggunakan PhpStorm yang sama.
Di awal file aturan, disarankan untuk memasukkan arahan yang menenangkan IDE favorit Anda:
<?php
Eksperimen pertama saya dengan sintaks dan kemungkinan filter untuk templat adalah phpgrep . Ini mungkin berguna sendiri, tetapi di dalam NoVerify telah menjadi lebih menarik, karena sekarang ia memiliki akses untuk mengetik informasi.
Beberapa kolega saya sudah mencoba phpgrep dalam pekerjaan mereka, dan ini adalah argumen lain yang mendukung pemilihan sintaksis seperti itu.
Phpgrep sendiri merupakan adaptasi gogrep untuk PHP (Anda mungkin juga tertarik dengan cgrep ). Dengan menggunakan program ini, Anda dapat mencari kode melalui templat sintaks .
Alternatif adalah sintaks pencarian struktural dan penggantian (SSR) dari PhpStorm. Keuntungannya jelas - ini adalah format yang ada, tetapi saya mengetahui tentang fitur ini setelah saya mengimplementasikan phpgrep. Anda dapat, tentu saja, memberikan penjelasan teknis: ada sintaks yang tidak sesuai dengan PHP dan parser kami tidak akan menguasainya, tetapi alasan "nyata" yang meyakinkan ini ditemukan setelah menulis sepeda.
Bahkan, ada opsi lain
Mungkin diperlukan untuk menampilkan templat dengan kode-PHP hampir satu-ke-satu - atau pergi dengan cara lain: menciptakan bahasa baru, misalnya dengan sintaks ekspresi S.
PHP-like Lisp-like ----------------------------- $x = $y | (expr = $x $y) fn($x, 1) | (expr call fn $x 1) : (or (expr == (type string (expr)) (expr)) (expr == (expr) (type string (expr))))
Pada akhirnya, saya berpikir bahwa keterbacaan template masih penting, dan kita dapat menambahkan filter melalui atribut phpdoc.
dentang-permintaan adalah contoh dari ide yang sama, tetapi menggunakan sintaksis yang lebih tradisional.
Kami membuat dan menjalankan diagnostik kami sendiri!
Mari kita coba menerapkan diagnostik baru kami untuk penganalisa.
Untuk melakukan ini, Anda perlu menginstal NoVerify. Ambil rilis biner jika Anda tidak memiliki Go-toolchain di sistem (jika Anda memilikinya, Anda dapat mengkompilasi semuanya dari sumbernya).
Pernyataan masalah
PHP memiliki banyak fungsi menarik, salah satunya adalah parse_str . Tanda tangannya:
Anda akan mengerti apa yang salah di sini jika Anda melihat contoh ini dari dokumentasi:
$str = "first=value&arr[]=foo+bar&arr[]=baz"; parse_str($str); echo $first;
Mmm, parameter dari string berada dalam ruang lingkup saat ini. Untuk menghindari ini, kami akan meminta dalam pengujian baru kami untuk menggunakan parameter fungsi kedua, $result
, sehingga hasilnya ditulis ke array ini.
Buat diagnostik Anda sendiri
Buat file myrules.php
:
<?php parse_str($_);
File aturan secara umum adalah daftar ekspresi di tingkat atas, yang masing-masing ditafsirkan sebagai templat phpgrep. Komentar phpdoc khusus diharapkan untuk setiap templat tersebut. Hanya satu atribut yang diperlukan - kategori kesalahan dengan teks peringatan.
Sekarang ada empat level total: error
, warning
, info
, dan maybe
. Dua yang pertama sangat penting: linter akan mengembalikan kode non-nol setelah eksekusi jika setidaknya salah satu aturan penting berfungsi. Setelah atribut itu sendiri ada teks peringatan yang akan dikeluarkan oleh linter jika template dipicu.
Template yang kami tulis menggunakan $_
- ini adalah variabel template yang tidak disebutkan namanya. Kita bisa menyebutnya, misalnya, $x
, tetapi karena kita tidak melakukan apa-apa dengan variabel ini, kita dapat memberinya nama "kosong". Perbedaan antara variabel template dan variabel PHP adalah bahwa yang pertama bertepatan dengan ekspresi apa pun, dan tidak hanya dengan variabel "literal". Ini nyaman: kita sering perlu mencari ekspresi yang tidak diketahui, daripada variabel tertentu.
Memulai diagnostik baru
Buat file uji kecil untuk debugging, test.php
:
<?php function f($x) { parse_str($x);
Selanjutnya, jalankan NoVerify dengan aturan kami pada file ini:
$ noverify -rules myrules.php test.php
Peringatan kami akan terlihat seperti ini:
WARNING myrules.php:4: parse_str without second argument at test.php:4 parse_str($x); ^^^^^^^^^^^^^
Nama pemeriksaan default adalah nama file aturan dan baris yang menentukan pemeriksaan ini. Dalam kasus kami, ini adalah myrules.php:4
.
Anda dapat mengatur nama Anda menggunakan @name <name>
.
@Nama contoh
parse_str($_);
WARNING parseStrResult: parse_str without second argument at test.php:4 parse_str($x); ^^^^^^^^^^^^^
Aturan yang dinamai menyerah pada hukum diagnostik lain:
- Dapat dinonaktifkan melalui
-exclude-checks
- Tingkat
-critical
dapat didefinisikan ulang melalui -critical
Bekerja dengan tipe
Contoh sebelumnya bagus untuk hello world - tetapi seringkali kita perlu mengetahui jenis ekspresi untuk mengurangi jumlah operasi diagnostik
Sebagai contoh, untuk fungsi in_array, kami meminta argumen $strict=true
ketika argumen pertama ( $needle
) adalah tipe string.
Untuk ini kami memiliki filter hasil.
Salah satu filter tersebut adalah @type <type> <var>
. Ini memungkinkan Anda untuk membuang segala sesuatu yang tidak sesuai dengan tipe yang disebutkan.
in_array($needle, $_);
Di sini kami memberikan nama argumen pertama ke panggilan in_array
untuk mengikat filter tipe ke sana. Peringatan akan dikeluarkan hanya ketika tipe $needle
adalah string
.
Set filter dapat dikombinasikan dengan operator @or
:
$x == $y;
Pada contoh di atas, polanya hanya akan cocok dengan ekspresi ==
, di mana salah satu operan bertipe string
. Dapat diasumsikan bahwa tanpa @or
semua filter digabungkan melalui @and
, tetapi ini tidak perlu diindikasikan secara eksplisit.
Batasi ruang lingkup diagnosis
Untuk setiap pengujian, Anda dapat menentukan @scope <name>
:
@scope all
- nilai default, validasi berfungsi di mana saja;@scope root
- luncurkan hanya di tingkat atas;@scope local
- jalankan hanya di dalam fungsi dan metode.
Misalkan kita ingin melaporkan return
luar fungsi tubuh. Dalam PHP, ini kadang masuk akal - misalnya, ketika file terhubung dari suatu fungsi ... Tapi dalam artikel ini kami mengutuk ini.
return $_;
Mari kita lihat bagaimana aturan ini akan berlaku:
<?php function f() { return "OK"; } return "NOT OK";
Demikian pula, Anda dapat membuat permintaan untuk menggunakan *_once
alih-alih require
dan include
:
require $_; include $_;
Sekarang ketika mencocokkan pola, tanda kurung tidak diperhitungkan dengan cukup konsisten. Pola (($x))
tidak akan menemukan "semua ekspresi dalam tanda kurung ganda", tetapi hanya sembarang ekspresi, mengabaikan tanda kurung. Namun, $x+$y*$z
dan ($x+$y)*$z
berlaku sebagaimana mestinya. Fitur ini berasal dari kesulitan bekerja dengan token (
dan )
, tetapi ada kemungkinan bahwa pesanan akan dikembalikan dalam salah satu rilis berikutnya.
Template Pengelompokan
Ketika duplikasi komentar phpdoc muncul di templat, kemampuan untuk menggabungkan templat datang ke penyelamatan.
Contoh sederhana untuk menunjukkan:
Sekarang bayangkan betapa tidak menyenangkannya menggambarkan sebuah aturan dalam contoh berikut tanpa fitur ini!
{ $x > $y; $x < $y; $x >= $y; $x <= $y; $x == $y; }
Format rekaman yang ditentukan dalam artikel hanyalah salah satu opsi yang diusulkan. Jika Anda ingin berpartisipasi dalam pilihan, maka Anda memiliki kesempatan seperti itu: Anda harus memberi +1 pada tawaran yang lebih Anda sukai daripada yang lain. Untuk lebih jelasnya, klik di sini .
Bagaimana aturan dinamis diintegrasikan

Pada saat peluncuran, NoVerify mencoba menemukan file aturan yang ditentukan dalam argumen rules
.
Selanjutnya, file ini diuraikan sebagai skrip PHP biasa, dan dari AST yang dihasilkan, seperangkat objek aturan dengan templat phpgrep yang terikat dengannya dikumpulkan.
Kemudian penganalisa memulai pekerjaan sesuai dengan skema yang biasa - satu-satunya perbedaan adalah bahwa untuk beberapa bagian kode yang diperiksa, ia memulai seperangkat aturan yang terikat. Jika aturan dipicu, peringatan ditampilkan.
Sukses dianggap sebagai pencocokan template phpgrep dan bagian dari setidaknya satu set filter (mereka dipisahkan oleh @or
).
Pada tahap ini, mekanisme aturan tidak secara signifikan memperlambat pengoperasian linter, bahkan jika ada banyak aturan dinamis.
Algoritma Pencocokan
Dengan pendekatan naif, untuk setiap simpul AST, kita perlu menerapkan semua aturan dinamis. Ini adalah implementasi yang sangat tidak efisien, karena sebagian besar pekerjaan akan dilakukan dengan sia-sia: banyak template memiliki awalan spesifik yang dengannya kita dapat mengelompokkan aturan.
Ini mirip dengan gagasan pencocokan paralel , tetapi alih-alih dengan jujur membangun NFA, kami hanya "memparalelkan" langkah pertama perhitungan.
Pertimbangkan ini dengan contoh dengan tiga aturan:
$_ ? $x : $x; explode("", ${"*"}); if ($_);
Jika kita memiliki elemen N dan aturan M, dengan pendekatan naif kita memiliki operasi N * M untuk dilakukan. Secara teori, kompleksitas ini dapat dikurangi menjadi linier dan mendapatkan O(N)
- jika Anda menggabungkan semua pola menjadi satu dan melakukan kecocokan seperti halnya, misalnya, paket regexp dari Go.
Namun, dalam praktiknya, saya sejauh ini fokus pada implementasi parsial dari pendekatan ini. Ini akan memungkinkan aturan dari file di atas untuk dibagi menjadi tiga kategori, dan untuk elemen AST yang tidak ada aturan yang sesuai, untuk menetapkan kategori kosong keempat. Karena itu, tidak lebih dari satu aturan yang dijalankan untuk setiap elemen.
Jika kita memiliki ribuan aturan dan kita akan merasakan pelambatan yang signifikan, algoritme akan diselesaikan. Sementara itu, kesederhanaan solusi dan akselerasi yang dihasilkan cocok untuk saya.
Sintaksis saat ini menduplikasi @var
dan @var
, tetapi kita mungkin memerlukan operator baru, misalnya, "tipe tidak sama." Bayangkan bagaimana tampilannya.
Kami memiliki setidaknya dua prioritas penting:
- Sintaksis penjelasan yang dapat dibaca dan ringkas.
- Dukungan setinggi mungkin dari IDE tanpa usaha ekstra.
Untuk PhpStorm ada plugin anotasi php yang menambahkan penyelesaian otomatis, transisi ke kelas anotasi dan kegunaan lain untuk bekerja dengan komentar phpdoc.
Prioritas (2) dalam praktik berarti Anda membuat keputusan yang tidak bertentangan dengan harapan IDE dan plugin. Misalnya, Anda dapat membuat anotasi dalam format yang dapat dikenali oleh plugin anotasi php:
class Filter { public $value; public $type; public $text; }
Kemudian menerapkan filter ke tipe akan terlihat seperti ini:
@Type($needle, eq=string) @Type($x, not_eq=Foo)
Pengguna dapat pergi ke definisi Filter
, mereka akan diminta dengan daftar parameter yang mungkin (tipe / teks / dll).
Metode perekaman alternatif, beberapa di antaranya disarankan oleh rekan:
@type string $needle @type !Foo $x @type $needle == string @type $x != Foo @type(==) string $needle @type(!=) Foo $x @type($needle) == string @type($x) != Foo @filter type($needle) == string @filter type($x) != Foo
Kemudian kami sedikit terganggu dan lupa bahwa semuanya ada di dalam phpdoc, dan ini muncul:
(eq string (typeof $needle)) (neq Foo (typeof $x))
Meskipun opsi dengan perekaman postfix untuk bersenang-senang juga terdengar. Bahasa untuk menggambarkan batasan tipe dan nilai bisa disebut keenam:
@eval string $needle typeof = @eval Foo $x typeof <>
Pencarian untuk opsi terbaik masih belum selesai ...
Perbandingan Extensibility dengan Phan
Sebagai salah satu kelebihan Phan , artikel " Analisis statis kode PHP menggunakan contoh PHPStan, Phan dan Mazmur " menunjukkan ekstensibilitas.
Inilah yang diimplementasikan dalam plugin sampel:
Kami ingin mengevaluasi seberapa siap kode kami untuk PHP 7.3 (khususnya, untuk mengetahui apakah ia memiliki konstanta case-insensitive). Kami hampir yakin bahwa tidak ada konstanta seperti itu, tetapi apa pun bisa terjadi dalam 12 tahun - itu harus diperiksa. Dan kami menulis sebuah plugin untuk Phan yang akan bersumpah jika parameter ketiga digunakan dalam define ().
Beginilah tampilan kode plugin (pemformatan dioptimalkan untuk lebar):
<?php use Phan\AST\ContextNode; use Phan\CodeBase; use Phan\Language\Context; use Phan\Language\Element\Func; use Phan\PluginV2; use Phan\PluginV2\AnalyzeFunctionCallCapability; use ast\Node; class DefineThirdParamTrue extends PluginV2 implements AnalyzeFunctionCallCapability { public function getAnalyzeFunctionCallClosures(CodeBase $code_base) { $def = function(CodeBase $cb, Context $ctx, Func $fn, $args) { if (count($args) < 3) { return; } $this->emitIssue( $cb, $ctx, 'PhanDefineCaseInsensitiv', 'define with 3 arguments', [] ); }; return ['define' => $def]; } } return new DefineThirdParamTrue();
Dan inilah cara ini dapat dilakukan di NoVerify:
<?php define($_, $_, $_);
Kami ingin mencapai hasil yang kira-kira sama - sehingga hal-hal sepele dapat dilakukan sesederhana mungkin.
Kesimpulan
Tautan, materi yang bermanfaat
Tautan penting dikumpulkan di sini, beberapa di antaranya mungkin sudah disebutkan dalam artikel, tetapi untuk kejelasan dan kenyamanan, saya mengumpulkannya di satu tempat.
Jika Anda membutuhkan lebih banyak contoh aturan yang dapat diterapkan, Anda dapat mengintip tes NoVerify .