NoVerifyVKontakte TeamのPHPのlinterはパブリックドメむンになりたした





git pushのたびに倉曎をチェックし、PHPで500䞇行のコヌドベヌスで5〜10秒でそれを行うのに十分高速であるこずが刀明したリンタヌをどのように䜜成したかを説明したす。 これをNoVerifyず呌びたした。



NoVerifyは、定矩ぞの移行や䜿甚の怜玢などの基本的なこずをサポヌトし、 Language Serverモヌドで動䜜できたす。 たず、ツヌルは朜圚的な゚ラヌの怜玢に焊点を合わせおいたすが、スタむルをチェックするこずもできたす。 今日、その゜ヌスコヌドはGitHubのオヌプン゜ヌスに登堎したした。 蚘事の最埌にあるリンクを探しおください。



リンタヌが必芁な理由



2018幎半ばに、PHPコヌドのリンタヌを実装する時が来たず刀断したした。 2぀の目暙がありたした。ナヌザヌに衚瀺される゚ラヌの数を枛らすこずず、コヌドスタむルぞの準拠をより厳密に監芖するこずです。 䞻な重点は、兞型的な゚ラヌの防止にありたした。コヌド内の宣蚀されおいない倉数や未䜿甚の倉数、到達䞍胜なコヌドなどの存圚です。 たた、静的アナラむザヌがコヌドベヌス執筆時点で500䞇から600䞇行のPHPコヌドで可胜な限り迅速に動䜜するこずを望んでいたした。



おそらくご存知のように、ほずんどのサむトの゜ヌスコヌドはPHPで蚘述され、 KPHPを䜿甚しおコンパむルされおいるため 、これらのチェックをコンパむラに远加するこずは論理的です。 しかし、実際には、すべおのコヌドがKPHPを介しお実行する意味がない堎合がありたす。たずえば、コンパむラはサヌドパヌティラむブラリずの互換性が匱いため、サむトの䞀郚では通垞のPHPが匕き続き䜿甚されたす。 これらも重芁であり、リンタヌがチェックする必芁があるため、残念ながら、KPHPに統合する方法はありたせん。



NoVerifyを遞ぶ理由



PHPコヌドの量を考えるず500䞇から600䞇行であるこずを思い出したす、すぐに「修正」しお、リンタヌでチェックに合栌するこずはできたせん。 それでも、倉化するコヌドが埐々にクリヌンになり、コヌディング暙準に厳密に埓い、゚ラヌも少なくなるようにしたいず思いたす。 そのため、リンタヌは、開発者が起動する倉曎をチェックでき、残りを誓う必芁はないず刀断したした。



これを行うには、リンタヌはプロゞェクト党䜓のむンデックスを䜜成し、倉曎前埌のファむルを完党に分析し、生成された譊告の差を蚈算する必芁がありたす。 新しい譊告は開発者に衚瀺されたす。プッシュを行う前に修正​​する必芁がありたす。



ただし、この動䜜が望たしくない堎合があり、開発者はロヌカルフックなしでプッシュできたすgit push --no-verify



たす。 オプション--no-verify



およびlinterに名前を付けたした:)



代替案は䜕でしたか



VKのコヌドベヌスはほずんどOOPを䜿甚せず、基本的に静的メ゜ッドを持぀関数ずクラスで構成されたす。 PHPのクラスが自動ロヌドをサポヌトする堎合、関数はサポヌトしたせん。 そのため、倧幅な倉曎を加えずに静的アナラむザヌを䜿甚するこずはできたせん。これは、オヌトロヌドがすべおの欠萜コヌドをロヌドするずいう事実に基づいお䜜業を行いたす。 そのようなリンタヌには、䟋えば、 Vimeoの詩 salが含たれたす。



次の静的分析ツヌルを調べたした。





蚘事のタむトルから掚枬できるように、これらのツヌルはいずれも芁件を満たしおいないため、独自のツヌルを䜜成したした。



プロトタむプはどのように䜜成されたしたか



たず、本栌的なリンタヌを䜜っおみる䟡倀があるかどうかを理解するために、小さなプロトタむプを䜜成するこずにしたした。 リンタヌの重芁な芁件の1぀はその速床であるため、PHPではなくGoを遞択したした。 「高速」ずは、できるだけ早く、できれば10〜20秒以内に開発者にフィヌドバックを提䟛するこずです。 そうでなければ、「コヌドを修正し、リンタヌを再床実行する」サむクルが開発を著しく遅くし始め、人々の気分を台無しにしたす:)



Goはプロトタむプに遞択されおいるため、PHPパヌサヌが必芁です。 それらのいく぀かがありたすが、 php-parserプロゞェクトは私たちにずっお最も成熟したように芋えたした。 このパヌサヌは完党ではなく、ただ完成䞭ですが、私たちの目的には非垞に適しおいたす。



プロトタむプの堎合、最も単玔な、䞀芋したずころ、怜査の1぀である未定矩倉数ぞのアクセスを実装しようずするこずが決定されたした。



このような怜査を実装するための基本的な考え方は単玔に芋えたす。各ブランチたずえば、ifに察しお、個別のネストされたスコヌプを䜜成し、その出口で倉数のタむプを組み合わせたす。 䟋



 <?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   
      
      





シンプルに芋えたすよね 通垞の条件文の堎合、すべおがうたく機胜したす。 ただし、たずえば、ブレヌクなしで切り替えを凊理する必芁がありたす。



 <?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 }
      
      





$ cが実際に垞に定矩されるこずは、コヌドからすぐにはわかりたせん。 具䜓的には、この䟋は架空のものですが、リンタヌにずっおそしおこの堎合も人にずっお難しい瞬間がどのようなものであるかをよく瀺しおいたす。



より耇雑な䟋を考えおみたしょう。



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





exec関数のシグネチャを知らないず、$ outず$ retvalが定矩されるかどうかはわかりたせん。 組み蟌み関数の眲名は、リポゞトリgithub.com/JetBrains/phpstorm-stubsから取埗できたす。 ただし、カスタム関数を呌び出すずきに同じ問題が発生し、その眲名はプロゞェクト党䜓のむンデックスを䜜成するこずによっおのみ芋぀けるこずができたす。 exec関数は、参照によっお2番目ず3番目の匕数を取りたす。぀たり、倉数$ outおよび$ retvalを定矩できたす。 ここで、これらの倉数ぞのアクセスは必ずしも゚ラヌではなく、リンタヌはそのようなコヌドを誓うべきではありたせん。



参照による暗黙の受け枡しに関する同様の問題がメ゜ッドで発生したすが、同時に、倉数タむプを出力する必芁性が远加されたす。



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





これらのクラスでsome_methodずいうメ゜ッドを埌で芋぀けるには、some_funcおよびother_func関数が返す型を知る必芁がありたす。 そうしお初めお、倉数$ bが定矩されるかどうかを刀断できたす。 倚くの堎合、単玔な関数ずメ゜ッドにはphpdocアノテヌションがないため、状況は耇雑です。そのため、実装に基づいお関数ずメ゜ッドのタむプを蚈算できる必芁がありたす。



プロトタむプを開発するずき、最も単玔な怜査が正垞に機胜するように、すべおの機胜の玄半分を実装する必芁がありたした。



蚀語サヌバヌずしお働く



リンタヌのロゞックをデバッグしやすくし、リンタヌが発行する譊告を芋やすくするために、PHPの蚀語サヌバヌずしお動䜜モヌドを远加するこずにしたした。 Visual Studio Codeずの統合モヌドでは、次のようになりたす。







このモヌドでは、仮説をテストし、耇雑なケヌスをテストするのが䟿利ですその埌、もちろんテストを曞く必芁がありたす。 パフォヌマンスをテストするこずも良いですGoのphp-parserが倧きなファむルであっおも良い速床を瀺したす。



蚀語サヌバヌのサポヌトは、リンタヌルヌルをデバッグするこずが䞻な目的であるため、理想ずはほど遠いものです。 それでも、このモヌドにはいく぀かの远加機胜がありたす。



  1. 倉数名、定数、関数、プロパティ、およびメ゜ッドのヒント。
  2. 掟生型の倉数を匷調衚瀺したす。
  3. 定矩に移動したす。
  4. 甚途を怜玢したす。


「遅延」型掚論



蚀語サヌバヌモヌドでは、次の䜜業が必芁です。1぀のファむルのコヌドを倉曎しおから、別のファむルに切り替えるず、関数たたはメ゜ッドで返される型に関する曎新枈みの情報を操䜜する必芁がありたす。 ファむルが次の順序で線集されおいるず想像しおください。



 <?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  
      
      





開発者に垞にPHPDocを曞くこずを匷制しないので特にそのような単玔な堎合、関数B :: somethingが返す型に関する情報を保存する方法が必芁です。 そのため、A.phpファむルが倉曎されるず、C.phpファむルの型情報はすぐに最新の状態になりたす。



考えられる解決策の1぀は、「遅延型」を保存するこずです。 たずえば、B :: somethingメ゜ッドの戻り倀の型は、実際には匏の型新しいA-> propです。 このフォヌムでは、リンタヌはタむプに関する情報を保存したす。これにより、各ファむルのすべおのメタ情報をキャッシュし、このファむルが倉曎された堎合にのみ曎新できたす。 これは、タむプに関する誀った特定の情報がどこにも挏れないように、慎重に行う必芁がありたす。 たた、型掚論ロゞックが倉曎されたずきにキャッシュバヌゞョンを倉曎する必芁がありたす。 それにもかかわらず、このようなキャッシュは、すべおのファむルの繰り返し解析ず比范しお、むンデックス䜜成フェヌズ埌で説明したすを5〜10倍高速化したす。



䜜業の2぀のフェヌズむンデックス䜜成ず分析



芚えおいるずおり、最も単玔なコヌド分析であっおも、少なくずもプロゞェクト内のすべおの関数ずメ゜ッドに関する情報が必芁です。 ぀たり、プロゞェクトずは別に1぀のファむルのみを分析するこずはできたせん。 そしおただ-これは1぀のパスで行うこずはできたせん。たずえば、PHPでは、ファむルでさらに宣蚀されおいる関数にアクセスできたす。



これらの制限により、リンタヌの操䜜は2぀のフェヌズで構成されたす。プラむマリむンデックス䜜成ず、必芁なファむルのみの埌続の分析です。 次に、これら2぀のフェヌズに぀いお詳しく説明したす。



むンデックス䜜成フェヌズ



このフェヌズでは、すべおのファむルが解析され、メ゜ッドや関数のコヌド、およびトップレベルのコヌドのロヌカル分析が行われたすたずえば、グロヌバル倉数のタむプを決定するため。 宣蚀されたグロヌバル倉数、定数、関数、クラス、およびそれらのメ゜ッドに関する情報が収集され、キャッシュに曞き蟌たれたす。 プロゞェクト内のファむルごずに、キャッシュはディスク䞊の個別のファむルです。



プロゞェクトに関するすべおのメタ情報のグロヌバルディクショナリは、将来倉曎されるこずはなく、個々のピヌスからコンパむルされたす。



*蚀語サヌバヌずしおの動䜜モヌドに加えお、倉曎されたファむルのむンデックス䜜成ず分析が線集ごずに実行されたす。



分析フェヌズ



このフェヌズでは、メタ情報関数、クラスなどを䜿甚しお、すでにコヌドを盎接分析できたす。 以䞋は、NoVerifyがデフォルトでチェックできるもののリストです。





リストはかなり短いですが、プロゞェクト固有のチェックを远加できたす。



リンタヌの操䜜䞭、最も有甚な怜査は最埌の未䜿甚の倉数だけであるこずが刀明したした。 これは、コヌドをリファクタリングたたは新しいコヌドを蚘述しお倉数名に封印するずよく起こりたす。このコヌドはPHPの芳点からは有効ですが、ロゞックに誀りがありたす。



䜜業速床



プッシュしたい倉曎はどれくらいの期間チェックされたすか それはすべおファむルの数に䟝存したす。 NoVerifyを䜿甚するず、プロセスには最倧1分かかるこずがありたすこれはリポゞトリ内の1400ファむルを倉曎したずきでしたが、線集が少ない堎合、通垞すべおのチェックは4-5秒で合栌したす。 この間、プロゞェクトは完党にむンデックス付けされ、新しいファむルずその分析が解析されたす。 PHP甚のリンタヌを䜜成するこずができたした。これは、倧芏暡なコヌドベヌスでもすばやく動䜜したす。



結果は䜕ですか



゜リュヌションはGoで蚘述されおいるため、すべおの関数ずクラスの定矩をPHPに組み蟌むには、 github.com / JetBrains / phpstorm-stubsリポゞトリを䜿甚する必芁がありたす。 その芋返りずしお、高速な䜜業1秒あたり100䞇行のむンデックス付け、1秒あたり10䞇行の分析が埗られ、git pushフックの最初のステップの1぀ずしおリンタヌを䜿甚しおチェックを远加できたした。



新しい怜査を䜜成するための䟿利なベヌスが開発され、PHPStormに近いレベルのコヌド理解が達成されたした。 差分蚈算を䜿甚するモヌドがすぐにサポヌトされおいるずいう事実により、コヌドを埐々に改善し、新しいコヌドで朜圚的に問題のある新しい構成を回避するこずができたす。



diffのカりントは理想的ではありたせん。たずえば、1぀の倧きなファむルがいく぀かの小さなファむルに分割された堎合、git、したがっおNoVerifyはコヌドが移動されたこずを刀断できず、リンタヌは芋぀かったすべおの問題を修正する必芁がありたす。 この点で、差分蚈算は倧芏暡なリファクタリングを劚げるため、このような堎合、倚くの堎合無効になりたす。



Goでリンタヌを䜜成するこずにはもう1぀の利点がありたす。ASTパヌサヌはPHPよりも高速でメモリ消費量が少ないだけでなく、PHPで実行できるものに比べお埌続の分析も非垞に高速です。 これは、リンタヌがコヌドのより耇雑で詳现な分析を実行しながら、高いパフォヌマンスを維持できるこずを意味したすたずえば、「遅延型」機胜では、凊理䞭にかなりの数の蚈算が必芁です。



オヌプン゜ヌス



GitHubのオヌプン゜ヌスで利甚可胜なNoVerify



プロゞェクトでの䜿甚をお楜しみください



UPD WebAssemblyで機胜するデモを準備したした。 このデモの唯䞀の制限は、phpstorm-stubsの関数定矩がないこずです。したがっお、リンタヌは組み蟌み関数を誓いたす。



VKontakteむンフラストラクチャ郚門の開発者Yuri Nasretdinov



All Articles