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

Learning PHP and Static Analysis with PHP Parser

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/

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が尊い。