Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Learning PHP and Static Analysis with PHP Parser

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.
Avatar for inouehi inouehi
March 09, 2024

Learning PHP and Static Analysis with PHP Parser

『PHP Parserで学ぶPHPと静的解析』

PHPerKaigi 2024
2024-03-09 14:40〜 Track A
https://phperkaigi.jp/2024/

Avatar for inouehi

inouehi

March 09, 2024
Tweet

More Decks by inouehi

Other Decks in Programming

Transcript

  1. 11 • PHPのコードを抽象構文木と呼ばれるオブジェクト群に変換する。 • オブジェクト1つ1つをノードと呼び、200種類弱のノードがある。 ◦ if → If_ノード ◦

    関数宣言 → Function_ノード ◦ = → Assignノード etc. • 静的解析ツールは、そのオブジェクトを操作することでコードを解析する。コードを書き換える ことだってできる。 PHP Parserの基礎知識 参考: PHP Parserで学ぶPHP
  2. 25 • 型宣言 ◦ 引数、戻り値、プロパティ • new • static ◦

    メソッドコール、プロパティアクセス etc. 1. コードの中からクラスを見つけ出す function foo(Bar $bar): Baz class Foo { private Bar $bar; } new Foo() Foo::bar Foo::baz() class Foo {}
  3. 31 1. グローバル関数の戻り値の型 2. 配列の型 3. 式の解決 4. PHPDocの解釈 5.

    self, parent, staticの読み替え 6. 並列処理 etc. 非対応事項
  4. 34 1. NodeFinderを用いてNameノードを取得する。 2. NodeFinderを用いてUse_ノードを取得する。 3. NodeFinderを用いてNamespace_ノードを取得する。 4. 1~3を組み合わせて修飾名を得る。 a.

    Nameノードからクラス名を得る。 b. クラス名が完全修飾名の場合、何もしない。 c. クラス名とUse_から得た名前空間を結合して、修飾名を得る。 d. cがない場合、Namespace_から得た名前空間を結合して、修飾名を得る。 クラスを見つけ出す
  5. 35 NodeFinderはASTからノードを取得する手軽な手段。 • callbackとして渡された条件に合うノードを取得する。 • 指定したノードを取得する。 クラスを見つけ出す - NodeFinder[1] 1.

    https://github.com/nikic/PHP-Parser/blob/ce019e9ad711e31ee87c2c4c72e538b5240970c3/doc/component/Walking_the_AST.markdown#s imple-node-finding $classes = $nodeFinder->findInstanceOf($ast, Node\Name::class); この中から探す Nameノードを探す
  6. 36 1. NodeFinderを用いてNameノードを取得する。 2. NodeFinderを用いてUse_ノードを取得する。 3. NodeFinderを用いてNamespace_ノードを取得する。 4. 1~3を組み合わせて修飾名を得る。 a.

    Nameノードからクラス名を得る。 b. クラス名が完全修飾名の場合、何もしない。 c. そうでない場合、クラス名とUse_から得た名前空間を結合して、修飾名を得る。 d. cがない場合、Namespace_から得た名前空間を結合して、修飾名を得る。 クラスを見つけ出す
  7. 37 1. NodeFinderを用いてNameノードを取得する。 2. NodeFinderを用いてUse_ノードを取得する。 3. NodeFinderを用いてNamespace_ノードを取得する。 4. 1~3を組み合わせて修飾名を得る。 a.

    Nameノードからクラス名を得る。 b. クラス名が完全修飾名の場合、何もしない。 c. そうでない場合、クラス名とUse_から得た名前空間を結合して、修飾名を得る。 d. cがない場合、Namespace_から得た名前空間を結合して、修飾名を得る。 クラスを見つけ出す 煩雑で ちょっと面倒…
  8. 40 NameResolverはクラス名等の名前を解決する。 クラスを見つけ出す - NameResolver $nameResolver = new PhpParser\NodeVisitor\NameResolver; $nodeTraverser

    = new PhpParser\NodeTraverser; $nodeTraverser->addVisitor($nameResolver); $ast = $nodeTraverser->traverse($ast); この中に含まれるクラス名等を 完全修飾名にできる
  9. 42 実装例: https://github.com/hirokinoue/phperkaigi-2024/blob/main/src/Example1/NameNodesExtractor.php /** * @return Name[] */ public function

    extractName(): array { // 名前を解決する $traverser = new NodeTraverser(); $traverser->addVisitor(new NameResolver()); $namedNodes = $traverser->traverse($this->ast); // Nameノードでフィルターする $nodeFinder = new NodeFinder(); return $nodeFinder->findInstanceOf($namedNodes, Name::class); } サンプルコード
  10. 45 クラス名が取得できるようになったが、クラス以外の名前も混在していた。 これらにはクラスのファイルが存在しない。 • 定数 • キーワード • 関数名 •

    特殊なクラス名: self, parent, static[1] • namespace[1] しかし、この実装には問題がある 1. Nameノードの代わりにFullyQualifiedノードでフィルターするとこれらは混入しない。 実装例: https://github.com/hirokinoue/phperkaigi-2024/tree/main/src/Example2
  11. 48 サンプルコード try { $reflector = new ReflectionClass($fullyQualified->toCodeString()); } catch

    (\ReflectionException $r) { // クラスではない場合: クラス名も内容も取得できない // 第一引数がクラス名、第二引数がクラスの内容 return new self('', ''); } // クラスの場合: クラス名は取得でき、内容はファイルの有無次第 $path = ($reflector->getFileName() === false) ? '' : $reflector->getFileName(); $code = self::readFile($path); return new self($fullyQualified->toCodeString(), $code); 実装例: https://github.com/hirokinoue/phperkaigi-2024/blob/main/src/Example3/ClassLoader.php
  12. 59 サンプルコード public function analyze(): DiagramUnit { $diagramUnit = new

    DiagramUnit($this->rootClassName()); $nodeTraverser = new NodeTraverser(); $nodeTraverser->addVisitor(new NameResolver()); $nodeTraverser->addVisitor(new ClassVisitor($diagramUnit)); $nodeTraverser->traverse($this->ast); return $diagramUnit; } 実装例: https://github.com/hirokinoue/phperkaigi-2024/tree/main/src/Example4
  13. 60 サンプルコード public function analyze(): DiagramUnit { $diagramUnit = new

    DiagramUnit($this->rootClassName()); $nodeTraverser = new NodeTraverser(); $nodeTraverser->addVisitor(new NameResolver()); $nodeTraverser->addVisitor(new ClassVisitor($diagramUnit)); $nodeTraverser->traverse($this->ast); return $diagramUnit; } 解析結果を 格納する箱
  14. 62 サンプルコード public function analyze(): DiagramUnit { $diagramUnit = new

    DiagramUnit($this->rootClassName()); $nodeTraverser = new NodeTraverser(); $nodeTraverser->addVisitor(new NameResolver()); $nodeTraverser->addVisitor(new ClassVisitor($diagramUnit)); $nodeTraverser->traverse($this->ast); return $diagramUnit; } 入力ファイルがクラスの場合、クラス名 そうでない場合、便宜的な名前とする
  15. 63 サンプルコード public function analyze(): DiagramUnit { $diagramUnit = new

    DiagramUnit($this->rootClassName()); $nodeTraverser = new NodeTraverser(); $nodeTraverser->addVisitor(new NameResolver()); $nodeTraverser->addVisitor(new ClassVisitor($diagramUnit)); $nodeTraverser->traverse($this->ast); return $diagramUnit; } クラスを見つけて ファイルを開き クラスを見つけて...
  16. 65 サンプルコード public function enterNode(Node $node) { if (!$node instanceof

    FullyQualified) { // ノードに対して何も処理せず次のノードへ return $node; } $classFile = ClassLoader::create($node); if ($classFile->isClass()) { // ノードがクラスだった場合の処理 } return $node; } ClassVisitor
  17. 66 サンプルコード public function enterNode(Node $node) { if (!$node instanceof

    FullyQualified) { // ノードに対して何も処理せず次のノードへ return $node; } $classFile = ClassLoader::create($node); if ($classFile->isClass()) { // ノードがクラスだった場合の処理 } return $node; } ClassVisitor
  18. 67 サンプルコード public function enterNode(Node $node) { if (!$node instanceof

    FullyQualified) { // ノードに対して何も処理せず次のノードへ return $node; } $classFile = ClassLoader::create($node); if ($classFile->isClass()) { // ノードがクラスだった場合の処理 } return $node; } ClassVisitor
  19. 68 サンプルコード public function enterNode(Node $node) { if (!$node instanceof

    FullyQualified) { // ノードに対して何も処理せず次のノードへ return $node; } $classFile = ClassLoader::create($node); if ($classFile->isClass()) { // ノードがクラスだった場合の処理 } return $node; } ClassVisitor
  20. 69 サンプルコード public function enterNode(Node $node) { if (!$node instanceof

    FullyQualified) { // ノードに対して何も処理せず次のノードへ return $node; } $classFile = ClassLoader::create($node); if ($classFile->isClass()) { // ノードがクラスだった場合の処理 } return $node; } ClassVisitor
  21. 70 サンプルコード public function enterNode(Node $node) { if (!$node instanceof

    FullyQualified) { // ノードに対して何も処理せず次のノードへ return $node; } $classFile = ClassLoader::create($node); if ($classFile->isClass()) { // ノードがクラスだった場合の処理 } return $node; } ClassVisitor
  22. 71 サンプルコード public function enterNode(Node $node) { if (!$node instanceof

    FullyQualified) { // ノードに対して何も処理せず次のノードへ return $node; } $classFile = ClassLoader::create($node); if ($classFile->isClass()) { // ノードがクラスだった場合の処理 } return $node; } ClassVisitor
  23. 72 サンプルコード if ($classFile->isClass()) { // 見つけたクラスをサブクラスとして格納する。 $subClass = new

    DiagramUnit($classFile->className()); $this->diagramUnit->push($subClass); // クラスのファイルがない場合、パースせずに次のノードへ if ($classFile->codeNotFound()) { return $node; } ClassVisitor
  24. 73 サンプルコード if ($classFile->isClass()) { // 前述 // クラスのファイルをパースしてASTを生成する $parser

    = (new ParserFactory())->createForHostVersion(); $ast = $parser->parse($classFile->content()); if ($ast === null) { // ASTが生成できない場合、次のノードへ return $node; } ClassVisitor
  25. 74 サンプルコード if ($classFile->isClass()) { // 前述 // 依存先クラスが依存するクラスを探しに行く $nodeTraverser

    = new NodeTraverser(); $nodeTraverser->addVisitor(new NameResolver()); $nodeTraverser->addVisitor(new ClassVisitor($subClass)); $nodeTraverser->traverse($ast); } ClassVisitor
  26. 81 設計見直し3 想定する3つのパターン • A→A … 自身への依存 • A→B→A …

    直接依存するクラスからの循環依存 • A→B→C→A … 2ステップ以上離れたクラスからの循環依存
  27. 82 設計見直し3 • DiagramUnitに依存元クラスの履歴を保持する。 • 読み込み済みのクラスは解析をスキップして次のノードへ。 例えば • A→B→C→A の場合、CはA,

    B, Cの履歴を保持しており Aへの依存を見つけた時、循環依存を検知する。 • この時、Aは読み込み済みなので解析しない。 実装例: https://github.com/hirokinoue/dependency-visualizer/pull/1/files
  28. 88 設計見直し4 1. クラスの中で、複数回同じクラスに依存する場合、結果を束ねる。 A B C class A {

    public function foo(): B {} public function bar(): B {} public function baz(): C {} }
  29. 90 設計見直し4 2. 別の枝で同じクラスに依存したらトラバースをやめる。 A B C D E D

    E A B C D E CがDに依存するとわかったら E以降の解析を止める
  30. 99 • 引数のオブジェクトはリファレンス渡し • 循環参照を言語仕様が許容 • 型(クラス)の導出に手間がかかるケース ◦ 式を解決して得られる型 ◦

    配列の型 ◦ PHPDocから得られる型 • self, parent, staticという特別なクラス名[1] • 定義済みのクラス[2]がありPHPで書かれていない PHPに関する事 1. https://github.com/nikic/PHP-Parser/blob/ce019e9ad711e31ee87c2c4c72e538b5240970c3/lib/PhpParser/Node/Name.php#L12 2. https://www.php.net/manual/ja/reserved.classes.php
  31. 100 • 引数のオブジェクトはリファレンス渡し • 循環参照を言語仕様が許容 • 型(クラス)の導出に手間がかかるケース ◦ 式を解決して得られる型 ◦

    配列の型 ◦ PHPDocから得られる型 • self, parent, staticという特別なクラス名[1] • 定義済みのクラス[2]がありPHPで書かれていない PHPに関する事 1. https://github.com/nikic/PHP-Parser/blob/ce019e9ad711e31ee87c2c4c72e538b5240970c3/lib/PhpParser/Node/Name.php#L12 2. https://www.php.net/manual/ja/reserved.classes.php そして、だからこそ PHPStanやPsalmが尊い。