Slide 1

Slide 1 text

Rectorと目指す 負債をためないシステム開発 〜はじめの一歩〜 2023/06/24 PHPカンファレンス福岡2023 1

Slide 2

Slide 2 text

About Me •Hiroki Inoue •Software Engineer •Engineering Manager @ WHITEPLUS, Inc. 2

Slide 3

Slide 3 text

Rectorに入門しようとする過程で整理したナレッジを共有します。 カスタムルールの書き方の理解を促す視点、ルールを書く際に利用できる クラス・メソッドを紹介し、その探し方を提案します。 Rectorの要素技術であるPHP Parserの仕組みにも触れつつ説明します。 トーク概要 https://fortee.jp/phpconfukuoka-2023/proposal/7be66059-fdda-4b75-b0e1-aad0d1a26d6a 3

Slide 4

Slide 4 text

誰向けのトーク? 1. Rectorのカスタムルールを書くことにハードルを感じたことがある方 2. 技術的負債の返済方法を模索している方 3. 静的解析の仕組みに興味がある方 4

Slide 5

Slide 5 text

本トークの目指すところ 1. Rectorのカスタムルールを書き始めるまでのハードルを下げる。 2. それによりRectorを活用しやすくし、負債解消手段の選択肢を増やす。 3. 静的解析の仕組みについて少し理解を深める。 5

Slide 6

Slide 6 text

アジェンダ 1. はじめに 2. Rectorとは 3. Rectorの使い方 4. カスタムルール作成のナレッジ 1) メンタルモデル 2) 分析対象コードの読み込みとノードの特定 3) ルール本体の書き方 4) 既製品の活用 5. まとめ 6

Slide 7

Slide 7 text

はじめに 7

Slide 8

Slide 8 text

Rectorの仕様が変わりこの資料はじきに陳腐化するはず。 ⇨賞味期限の長い知恵を抽出してお持ち帰りいただけると幸いです。 ルールを書くにはRector, PHP Parser, PHPStanや PHPに親しむ必要がある。 ⇨このトークは前者の理解を助けることを目指します。 8

Slide 9

Slide 9 text

フィードバックを是非お願いしますmm • わかりにくかった点 • 新たに発見した事 • 役に立った?立たなさそう? etc. 9

Slide 10

Slide 10 text

Rectorとは 10

Slide 11

Slide 11 text

Rectorにできること “Instant Upgrades and Automated Refactoring”[1] アプリケーションを即座にアップグレードしたり自動的にリファクタする。 1. https://github.com/rectorphp/rector 11

Slide 12

Slide 12 text

Rectorがやっていること 1. PHPのコードを読み取り 2. リファクタが必要かどうかを判断し 3. リファクタする。 12

Slide 13

Slide 13 text

Rectorの使い方 13

Slide 14

Slide 14 text

インストール/実行[1][2] 1. composerでインストールする。 2. rector.phpで検査対象、使用するルール等を指定する。 • initでrector.phpの雛形を生成できる。 3. 実行する。 • コードが修正される。 • dry runもできる。 1. https://getrector.com/documentation 2. 設定の書き方やコマンドは変わり得るので最新のドキュメントを参照のこと 14

Slide 15

Slide 15 text

カスタムルールの作成手順[1] 1. ルールを書く。 2. テストを書く。 3. composer.jsonでautoloadできるようにする。 4. rector.phpに使用するルールとして指定する。 5. 実行する。 1. https://getrector.com/documentation/custom-rule 15

Slide 16

Slide 16 text

カスタムルール作成のナレッジ 16

Slide 17

Slide 17 text

前提 $ ./vendor/bin/rector --version Rector 0.15.23 17

Slide 18

Slide 18 text

1) メンタルモデル 18

Slide 19

Slide 19 text

まずはRectorの振る舞いを確認する 19

Slide 20

Slide 20 text

https://www.php.net/manual/ja/language.oop5.decon.php#language.oop5.decon.constructor.promotion 20

Slide 21

Slide 21 text

class SomeClass { public string $name; public function __construct( string $name = 'Dario' ) { $this->name = $name; } } Constructor Property Promotion 特徴を分析して コンストラクタの中で staticでないプロパティに 引数と同じ変数を 設定している… 21

Slide 22

Slide 22 text

Constructor Property Promotion class SomeClass { public string $name; public function __construct( string $name = 'Dario' ) { $this->name = $name; } } class SomeClass { public function __construct( public string $name = 'Dario' ) { } } 22 修正する

Slide 23

Slide 23 text

テキストと捉えると複雑な事をしている様に感じる 23

Slide 24

Slide 24 text

オブジェクトとして捉えるとシンプルに感じる 24

Slide 25

Slide 25 text

オブジェクトとして捉える class SomeClass { public string $name; public function __construct( string $name = 'Dario' ) { $this->name = $name; } } 25

Slide 26

Slide 26 text

オブジェクトとして捉える ※簡略化しています class SomeClass { public string $name; public function __construct( string $name = 'Dario' ) { $this->name = $name; } } Class_ Property ClassMethod Expression Assign PropertyFetch Variable SomeClass private string $name; public function __construct(/** Param */) = $this->name $name Param string $name = 'Dario' 26

Slide 27

Slide 27 text

Class_ Property ClassMethod Expression Assign PropertyFetch Variable Param オブジェクトとして捉える ※簡略化しています class SomeClass { public string $name; public function __construct( string $name = 'Dario' ) { $this->name = $name; } } 27

Slide 28

Slide 28 text

Class_ Property ClassMethod Expression Assign PropertyFetch Variable Param オブジェクトとして捉える ※簡略化しています class SomeClass { public string $name; public function __construct( string $name = 'Dario' ) { $this->name = $name; } } 28

Slide 29

Slide 29 text

Class_ Property ClassMethod Expression Assign PropertyFetch Variable Param オブジェクトとして捉える ※簡略化しています class SomeClass { public string $name; public function __construct( string $name = 'Dario' ) { $this->name = $name; } } 29

Slide 30

Slide 30 text

Class_ Property ClassMethod Expression Assign PropertyFetch Variable Param オブジェクトとして捉える ※簡略化しています class SomeClass { public string $name; public function __construct( string $name = 'Dario' ) { $this->name = $name; } } 30

Slide 31

Slide 31 text

Class_ Property ClassMethod Expression Assign PropertyFetch Variable Param オブジェクトとして捉える ※簡略化しています class SomeClass { public string $name; public function __construct( string $name = 'Dario' ) { |$this->name = $name; } } 31

Slide 32

Slide 32 text

Class_ Property ClassMethod Expression Assign PropertyFetch Variable Param オブジェクトとして捉える ※簡略化しています class SomeClass { public string $name; public function __construct( string $name = 'Dario' ) { $this->name = $name; } } 32

Slide 33

Slide 33 text

Class_ Property ClassMethod Expression Assign PropertyFetch Variable Param オブジェクトとして捉える ※簡略化しています class SomeClass { public string $name; public function __construct( string $name = 'Dario' ) { $this->name = $name; } } 33

Slide 34

Slide 34 text

Class_ Property ClassMethod Expression Assign PropertyFetch Variable Param オブジェクトとして捉える ※簡略化しています class SomeClass { public string $name; public function __construct( string $name = 'Dario' ) { $this->name = $name; } } 34

Slide 35

Slide 35 text

オブジェクトとして捉える ※簡略化しています class SomeClass { public string $name; public function __construct( string $name = 'Dario' ) { $this->name = $name; } } Class_ Property ClassMethod Expression Assign PropertyFetch Variable SomeClass private string $name; public function __construct(/** Param */) = $this->name $name Param string $name = 'Dario' 35

Slide 36

Slide 36 text

オブジェクトとして捉える ※簡略化しています Class_ Property ClassMethod Expression Assign PropertyFetch Variable SomeClass private string $name; public function __construct(/** Param */) = $this->name $name Param string $name = 'Dario' 分析対象のノード 36

Slide 37

Slide 37 text

オブジェクトとして捉える ※簡略化しています Class_ Property ClassMethod Expression Assign PropertyFetch Variable SomeClass private string $name; public function __construct(/** Param */) = $this->name $name Param string $name = 'Dario' 分析対象のノード リファクタすべきかどうかを分析する 37

Slide 38

Slide 38 text

オブジェクトとして捉える ※簡略化しています Class_ Property ClassMethod Expression Assign PropertyFetch Variable SomeClass private string $name; public function __construct(/** Param */) = $this->name $name Param public string $name = 'Dario' 削除する 削除する 改変する 38

Slide 39

Slide 39 text

コードのテキストを書き換えるのではなく オブジェクトを改変すると捉える 39

Slide 40

Slide 40 text

※イメージ図 40

Slide 41

Slide 41 text

41

Slide 42

Slide 42 text

分析対象 削除 改変 削除 42

Slide 43

Slide 43 text

class SomeClass { public string $name; public function __construct( string $name = 'Dario' ) { $this->name = $name; } } 43

Slide 44

Slide 44 text

Class_ Property ClassMethod Expression Assign PropertyFetch Variable SomeClass private string $name; public function __construct(/** Param */) = $this->name $name Param string $name = 'Dario' 44

Slide 45

Slide 45 text

45

Slide 46

Slide 46 text

2) 分析対象コードの読み込みと ノードの特定 46

Slide 47

Slide 47 text

Rectorがやっていること(再掲) 1. PHPのコードを読み取り 2. リファクタが必要かどうかを判断し 3. リファクタする。 47

Slide 48

Slide 48 text

1. PHPのコードを読み取る PHP Parserを使ってPHPのコードを読み取り、PHPで処理できるようにオブ ジェクトの配列にする。 48

Slide 49

Slide 49 text

2. リファクタが必要かどうかを判断する • 分析対象ノードを特定する。 • そのノードに対してリファクタが必要かどうかを判断する。 49

Slide 50

Slide 50 text

2. リファクタが必要かどうかを判断する • 分析対象ノードを特定する。 • そのノードに対してリファクタが必要かどうかを判断する。 50

Slide 51

Slide 51 text

3. リファクタする ノード(オブジェクト)を改変する。 51

Slide 52

Slide 52 text

分析対象ノードの特定 public function getNodeTypes() : array { return [Class_::class]; } public function refactor(Node $node) : ?Node { } 52

Slide 53

Slide 53 text

public function getNodeTypes() : array { return [Class_::class]; } public function refactor(Node $node) : ?Node { } 分析対象ノードの特定 Class_ノードを分析対象とする 53

Slide 54

Slide 54 text

public function getNodeTypes() : array { return [Class_::class]; } public function refactor(Node $node) : ?Node { // リファクタが必要かどうかを判断する // ノード(オブジェクト)を改変する } 分析対象ノードの特定 Class_ノードを分析対象とする Class_ノードを受け取る 54

Slide 55

Slide 55 text

これ 55

Slide 56

Slide 56 text

ノードについて理解するのが重要 56

Slide 57

Slide 57 text

ノードについて理解する • ノードの種類 … PHP-Parser/lib/PhpParser/Node/[1] • ノードの種類 … PHPStan API documentation[2] • ノードのサンプル … PHP Parserで学ぶPHP[3] • ノードの生成方法 … Node Overview[4] 1. https://github.com/nikic/PHP-Parser/tree/master/lib/PhpParser/Node 2. https://apiref.phpstan.org/1.9.x/index.html 3. https://speakerdeck.com/inouehi/php-parserdexue-buphp?slide=24 4. https://github.com/rectorphp/php-parser-nodes-docs 57

Slide 58

Slide 58 text

PHP Parserの2つの側面 1. コードの読み取り • PHPのコードをパースして、PHPで処理できるオブジェクトにする。 2. コードの生成 • オブジェクト(ノード)を生成する。 58

Slide 59

Slide 59 text

コードの読み取り[1] 1. https://github.com/nikic/PHP-Parser/blob/4.x/doc/component/Walking_the_AST.markdown $traverser = new NodeTraverser; $traverser->addVisitor(new class extends NodeVisitorAbstract { public function beforeTraverse(array $nodes) { /** do something */ } public function enterNode(Node $node) { /** do something */ } public function leaveNode(Node $node) { /** do something */ } public function afterTraverse(array $nodes) { /** do something */ } }); $nodes = /** PHPのコードをパースしてノードを取り出す */ $traverser->traverse($nodes); 59

Slide 60

Slide 60 text

コードの読み取り $traverser = new NodeTraverser; $traverser->addVisitor(new class extends NodeVisitorAbstract { public function beforeTraverse(array $nodes) { /** do something */ } public function enterNode(Node $node) { /** do something */ } public function leaveNode(Node $node) { /** do something */ } public function afterTraverse(array $nodes) { /** do something */ } }); $nodes = /** PHPのコードをパースしてノードを取り出す */ $traverser->traverse($nodes); 60

Slide 61

Slide 61 text

コードの読み取り $traverser = new NodeTraverser; $traverser->addVisitor(new class extends NodeVisitorAbstract { public function beforeTraverse(array $nodes) { /** do something */ } public function enterNode(Node $node) { /** do something */ } public function leaveNode(Node $node) { /** do something */ } public function afterTraverse(array $nodes) { /** do something */ } }); $nodes = /** PHPのコードをパースしてノードを取り出す */ $traverser->traverse($nodes); 61

Slide 62

Slide 62 text

コードの読み取り $traverser = new NodeTraverser; $traverser->addVisitor(new class extends NodeVisitorAbstract { public function beforeTraverse(array $nodes) { /** do something */ } public function enterNode(Node $node) { /** do something */ } public function leaveNode(Node $node) { /** do something */ } public function afterTraverse(array $nodes) { /** do something */ } }); $nodes = /** PHPのコードをパースしてノードを取り出す */ $traverser->traverse($nodes); 62

Slide 63

Slide 63 text

コードの読み取り • Traverser … 2つの側面を持つ。 1. Nodeを一つ一つたどる。 2. Visitorを持ちNodesを受け取りVisitorにNodeを処理させる。 • Visitor … 処理の実装を持つ。 • Nodes … 処理対象。 63

Slide 64

Slide 64 text

64

Slide 65

Slide 65 text

コードの読み取り class Traverser { protected $visitors = []; protected function traverseNode(Node $node) { foreach ($this->visitors as $visitor) { $visitor->enterNode($node); } } public function addVisitor(Visitor $visitor) {} public function removeVisitor(Visitor $visitor) {} } ※イメージ図です 65

Slide 66

Slide 66 text

コードの読み取り class Traverser { protected $visitors = []; protected function traverseNode(Node $node) { foreach ($this->visitors as $visitor) { $visitor->enterNode($node); } } public function addVisitor(Visitor $visitor) {} public function removeVisitor(Visitor $visitor) {} } ※イメージ図です 66

Slide 67

Slide 67 text

コードの読み取り class Traverser { protected $visitors = []; protected function traverseNode(Node $node) { foreach ($this->visitors as $visitor) { $visitor->enterNode($node); } } public function addVisitor(Visitor $visitor) {} public function removeVisitor(Visitor $visitor) {} } ※イメージ図です 67

Slide 68

Slide 68 text

コードの読み取り class Traverser { protected $visitors = []; protected function traverseNode(Node $node) { foreach ($this->visitors as $visitor) { $visitor->enterNode($node); } } public function addVisitor(Visitor $visitor) {} public function removeVisitor(Visitor $visitor) {} } ※イメージ図です 68

Slide 69

Slide 69 text

コードの読み取り class Traverser { protected $visitors = []; protected function traverseNode(Node $node) { foreach ($this->visitors as $visitor) { $visitor->enterNode($node); } } public function addVisitor(Visitor $visitor) {} public function removeVisitor(Visitor $visitor) {} } ※イメージ図です 69

Slide 70

Slide 70 text

コードの読み取り class Traverser { protected $visitors = []; protected function traverseNode(Node $node) { foreach ($this->visitors as $visitor) { $visitor->enterNode($node); } } public function addVisitor(Visitor $visitor) {} public function removeVisitor(Visitor $visitor) {} } ※イメージ図です 70

Slide 71

Slide 71 text

ところで 71

Slide 72

Slide 72 text

RectorのルールはVisitor 72

Slide 73

Slide 73 text

付録 73

Slide 74

Slide 74 text

Traverserの実装例 NodeTraverser public function traverse(array $nodes) : array { $this->stopTraversal = false; foreach ($this->visitors as $visitor) { if (null !== $return = $visitor->beforeTraverse($nodes)) {$nodes = $return;} } $nodes = $this->traverseArray($nodes); // 後述 foreach ($this->visitors as $visitor) { if (null !== $return = $visitor->afterTraverse($nodes)) {$nodes = $return;} } return $nodes; } ※紙面の都合で大部分を省略しています 74

Slide 75

Slide 75 text

Traverserの実装例 NodeTraverser protected function traverseArray(array $nodes) : array { foreach ($nodes as $i => &$node) { if ($node instanceof Node) { foreach ($this->visitors as $visitorIndex => $visitor) { $return = $visitor->enterNode($node); } if ($traverseChildren) { $node = $this->traverseNode($node); // 後述 } foreach ($this->visitors as $visitorIndex => $visitor) { $return = $visitor->leaveNode($node); } } ※紙面の都合で大部分を省略しています 75

Slide 76

Slide 76 text

Traverserの実装例 NodeTraverser protected function traverseNode(\PhpParser\Node $node) : \PhpParser\Node { foreach ($node->getSubNodeNames() as $name) { $subNode =& $node->{$name}; if (\is_array($subNode)) { $subNode = $this->traverseArray($subNode); } elseif ($subNode instanceof \PhpParser\Node) { // 後述 } } ※紙面の都合で大部分を省略しています 76

Slide 77

Slide 77 text

Traverserの実装例 NodeTraverser foreach ($this->visitors as $visitorIndex => $visitor) { $return = $visitor->enterNode($subNode); } if ($traverseChildren) { $subNode = $this->traverseNode($subNode); } foreach ($this->visitors as $visitorIndex => $visitor) { $return = $visitor->leaveNode($subNode); } ※紙面の都合で大部分を省略しています 77

Slide 78

Slide 78 text

ルールクラスの成り立ち 1. RectorのルールはAbstractRectorを拡張する。 2. AbstractRectorはNodeVisitorAbstractを拡張する。 78

Slide 79

Slide 79 text

ルールクラスの成り立ち 1. RectorのルールはAbstractRectorを拡張する。 2. AbstractRectorはNodeVisitorAbstractを拡張する。 3. NodeVisitorAbstractはbeforeTraverse(), enterNode(), leaveNode(), afterTraverse()を持つ。 79

Slide 80

Slide 80 text

ルールクラスの成り立ち 1. RectorのルールはAbstractRectorを拡張する。 2. AbstractRectorはNodeVisitorAbstractを拡張する。 3. NodeVisitorAbstractはbeforeTraverse(), enterNode(), leaveNode(), afterTraverse()を持つ。 4. AbstractRectorはenterNode()の中で、前述したgetNodeTypes()やrefactor() を呼び出す。 80

Slide 81

Slide 81 text

ルールクラスの成り立ち 1. RectorのルールはAbstractRectorを拡張する。 2. AbstractRectorはNodeVisitorAbstractを拡張する。 3. NodeVisitorAbstractはbeforeTraverse(), enterNode(), leaveNode(), afterTraverse()を持つ。 4. AbstractRectorはenterNode()の中で、前述したgetNodeTypes()やrefactor() を呼び出す。 5. RectorのルールはgetNodeTypes()やrefactor()を実装する。 81

Slide 82

Slide 82 text

付録ここまで 82

Slide 83

Slide 83 text

3) ルール本体の書き方 83

Slide 84

Slide 84 text

公式サンプルの事例[1] 1. https://getrector.com/documentation/custom-rule $user = new User(); -$user->setPassword('123456'); +$user->changePassword('123456'); メソッド名を変更するRector 84

Slide 85

Slide 85 text

公式サンプルの事例 public function getNodeTypes(): array { return [MethodCall::class]; } 85

Slide 86

Slide 86 text

公式サンプルの事例 public function refactor(Node $node): ?Node { if (! $this->isName($node->name, 'set*')) { return null; } $methodCallName = $this->getName($node->name); $newMethodCallName = preg_replace('#^set#', 'change', $methodCallName); $node->name = new Identifier($newMethodCallName); return $node; } 86

Slide 87

Slide 87 text

公式サンプルの事例 public function refactor(Node $node): ?Node { // リファクタが必要かどうかを判断する if (! $this->isName($node->name, 'set*')) { return null; } $methodCallName = $this->getName($node->name); $newMethodCallName = preg_replace('#^set#', 'change', $methodCallName); $node->name = new Identifier($newMethodCallName); return $node; } 87

Slide 88

Slide 88 text

公式サンプルの事例 public function refactor(Node $node): ?Node { if (! $this->isName($node->name, 'set*')) { return null; } // ノード(オブジェクト)を改変する $methodCallName = $this->getName($node->name); $newMethodCallName = preg_replace('#^set#', 'change', $methodCallName); $node->name = new Identifier($newMethodCallName); return $node; } 88

Slide 89

Slide 89 text

公式サンプルの事例 public function refactor(Node $node): ?Node { if (! $this->isName($node->name, 'set*')) { return null; } // ノード(オブジェクト)を改変する $methodCallName = $this->getName($node->name); $newMethodCallName = preg_replace('#^set#', 'change', $methodCallName); $node->name = new Identifier($newMethodCallName); return $node; } 89

Slide 90

Slide 90 text

公式サンプルの事例 public function refactor(Node $node): ?Node { if (! $this->isName($node->name, 'set*')) { return null; } // ノード(オブジェクト)を改変する $methodCallName = $this->getName($node->name); $newMethodCallName = preg_replace('#^set#', 'change', $methodCallName); $node->name = new Identifier($newMethodCallName); return $node; } 90

Slide 91

Slide 91 text

4) 既製品の活用 91

Slide 92

Slide 92 text

既成のメソッド public function refactor(Node $node): ?Node { if (! $this->isName($node->name, 'set*')) { return null; } $methodCallName = $this->getName($node->name); $newMethodCallName = preg_replace('#^set#', 'change', $methodCallName); $node->name = new Identifier($newMethodCallName); return $node; } 92

Slide 93

Slide 93 text

AbstractRector final class MyFirstRector extends AbstractRector { public function getNodeTypes(): array {} public function refactor(Node $node): ?Node {} public function getRuleDefinition(): RuleDefinition {} } 93

Slide 94

Slide 94 text

AbstractRector • isName() • isNames() • getName() • isObjectType() • getType() • traverseNodesWithCallable() • mirrorComments() • appendArgs() • removeNode() 94

Slide 95

Slide 95 text

ケーススタディ(1/3) ルールの中で型を評価したい!! 95

Slide 96

Slide 96 text

たとえば 型を宣言する@varタグを追加するルール (VarConstantCommentRector) 96

Slide 97

Slide 97 text

class SomeClass { const HI = 'hi'; } class SomeClass { /** * @var string */ const HI = 'hi'; } 97

Slide 98

Slide 98 text

class SomeClass { const HI = 'hi'; } class SomeClass { /** * @var string */ const HI = 'hi'; } 98

Slide 99

Slide 99 text

getType()の使用例 /** * @param ClassConst $node */ public function refactor(Node $node) : ?Node { if (\count($node->consts) > 1) { // const F = 'f', B = 'b'; という書き方が可能 [1]なことを考慮 return null; } $constType = $this->getType($node->consts[0]->value); if ($constType instanceof MixedType) { return null; } ※紙面の都合で大部分を省略しています 1. https://speakerdeck.com/inouehi/php-parserdexue-buphp?slide=59 99

Slide 100

Slide 100 text

getType()の使用例 /** * @param ClassConst $node */ public function refactor(Node $node) : ?Node { if (\count($node->consts) > 1) { return null; } $constType = $this->getType($node->consts[0]->value); if ($constType instanceof MixedType) { return null; } ※紙面の都合で大部分を省略しています 100

Slide 101

Slide 101 text

getType()の使用例 /** * @param ClassConst $node */ public function refactor(Node $node) : ?Node { if (\count($node->consts) > 1) { return null; } $constType = $this->getType($node->consts[0]->value); if ($constType instanceof MixedType) { return null; } ※紙面の都合で大部分を省略しています 101

Slide 102

Slide 102 text

getType()の使用例 /** * @param ClassConst $node */ public function refactor(Node $node) : ?Node { if (\count($node->consts) > 1) { return null; } $constType = $this->getType($node->consts[0]->value); if ($constType instanceof MixedType) { return null; } ※紙面の都合で大部分を省略しています 102

Slide 103

Slide 103 text

ClassConst int ※flags AttributeGroup[] const HI = 'hi'; Const_[0] Identifier Expr ※value null null HI 'hi' HI = 'hi'; 103

Slide 104

Slide 104 text

getType()の実装 public function getType(Node $node) : Type { if ($node instanceof Property && $node->type instanceof NullableType) { } if ($node instanceof NullableType) { } if ($node instanceof Ternary) { } if ($node instanceof Coalesce) { } $type = $this->resolveByNodeTypeResolvers($node); ※紙面の都合で大部分を省略しています 104

Slide 105

Slide 105 text

getType()の実装 public function getType(Node $node) : Type { if ($node instanceof Property && $node->type instanceof NullableType) { } if ($node instanceof NullableType) { } if ($node instanceof Ternary) { } if ($node instanceof Coalesce) { } $type = $this->resolveByNodeTypeResolvers($node); ※紙面の都合で大部分を省略しています 105

Slide 106

Slide 106 text

getType()の存在意義 • ノードの種類によって型の導出方法が違う。 • PHP Parserは型を評価しない。 106

Slide 107

Slide 107 text

getType()の存在意義 • ノードの種類によって型の導出方法が違う。 • PHP Parserは型を評価しない。 ⇨クライアント(Rector)が型の導出ロジックを実装する必要がある。 107

Slide 108

Slide 108 text

getType()の存在意義 • ノードの種類によって型の導出方法が違う。 • PHP Parserは型を評価しない。 ⇨クライアント(Rector)が型の導出ロジックを実装する必要がある。 ⇨型を導出するメソッド(getType())が提供されている。 108

Slide 109

Slide 109 text

getType()の実装 • 型の導出ロジックは複雑。 109

Slide 110

Slide 110 text

getType()の実装 • 型の導出ロジックは複雑。 ⇨PHPStanを活用する。 110

Slide 111

Slide 111 text

PHP ParserとPHPStanの橋渡し • RectorによるリファクタはPHP Parserが定義するノードで表現される。 • getType()が返すのはPHPStanが定義する型。 111

Slide 112

Slide 112 text

PHP ParserとPHPStanの橋渡し • RectorによるリファクタはPHP Parserが定義するノードで表現される。 • getType()が返すのはPHPStanが定義する型。 ⇨変換するためのメソッド[1]が提供されている。 1. StaticTypeMapperのmapPHPStanTypeToPhpParserNodeなど。 112

Slide 113

Slide 113 text

RectorはPHPStanを活用する 113

Slide 114

Slide 114 text

1. https://phpstan.org/developing-extensions/scope Scope[1] "The PHPStan\Analyser\Scope object can be used to get more information about the code, like types of variables, or current file and namespace." Scopeオブジェクトを使用すると、変数の型や現在のファイルや名前空間といった コードに関する情報を取得できます。 114

Slide 115

Slide 115 text

Scope[1] public function doFoo(?array $list): void { // $listの型は配列かnullのいずれかである if ($list !== null) { // $listの型は配列である $foo = true; // $fooはtrueである } else { // $listの型はnullである $foo = false; // $fooはfalseである } // $listの型は配列かnullのいずれかである // $fooの型はboolである } 1. https://phpstan.org/developing-extensions/scope 115

Slide 116

Slide 116 text

Scope[1] public function doFoo(?array $list): void { // $listの型は配列かnullのいずれかである if ($list !== null) { // $listの型は配列である $foo = true; // $fooはtrueである } else { // $listの型はnullである $foo = false; // $fooはfalseである } // $listの型は配列かnullのいずれかである // $fooの型はboolである } 1. https://phpstan.org/developing-extensions/scope Variableノードは型の情報を持たないが doFoo()の引数から型を導出できる。 116

Slide 117

Slide 117 text

Scope[1] public function doFoo(?array $list): void { // $listの型は配列かnullのいずれかである if ($list !== null) { // $listの型は配列である $foo = true; // $fooはtrueである } else { // $listの型はnullである $foo = false; // $fooはfalseである } // $listの型は配列かnullのいずれかである // $fooの型はboolである } 1. https://phpstan.org/developing-extensions/scope ifの中ではnullでないことが確約されるため $listの型は配列であると判断できる。 117

Slide 118

Slide 118 text

ケーススタディ(2/3) ルールの中で子ノードを辿って処理したい!! 118

Slide 119

Slide 119 text

たとえば 例外クラスと変数名を一致させるルール (CatchExceptionNameMatchingTypeRector) 119

Slide 120

Slide 120 text

class SomeClass { public function run() { try { // ... } catch (SomeException $typoException) { $typoException->getTraceAsString(); $typoException->getMessage(); } } } class SomeClass { public function run() { try { // ... } catch (SomeException $someException) { $someException->getTraceAsString(); $someException->getMessage(); } } } 120

Slide 121

Slide 121 text

traverseNodesWithCallable() private function renameVariableInStmts(Catch_ $catch, string $oldName, string $newName) : void { $this->traverseNodesWithCallable( $catch->stmts, function (Node $node) use($oldName, $newName) { if (!$node instanceof Variable) { return null; } if (!$this->nodeNameResolver->isName($node, $oldName)) { return null; } $node->name = $newName; return null; } ); ※紙面の都合でアレンジしています 121

Slide 122

Slide 122 text

private function renameVariableInStmts(Catch_ $catch, string $oldName, string $newName) : void { $this->traverseNodesWithCallable( $catch->stmts, function (Node $node) use($oldName, $newName) { if (!$node instanceof Variable) { return null; } if (!$this->nodeNameResolver->isName($node, $oldName)) { return null; } $node->name = $newName; return null; } ); traverseNodesWithCallable() } catch (SomeException $typoException) { $typoException->getTraceAsString(); $typoException->getMessage(); } 122 ※紙面の都合でアレンジしています

Slide 123

Slide 123 text

private function renameVariableInStmts(Catch_ $catch, string $oldName, string $newName) : void { $this->traverseNodesWithCallable( $catch->stmts, function (Node $node) use($oldName, $newName) { if (!$node instanceof Variable) { return null; } if (!$this->nodeNameResolver->isName($node, $oldName)) { return null; } $node->name = $newName; return null; } ); traverseNodesWithCallable() 123 ※紙面の都合でアレンジしています

Slide 124

Slide 124 text

private function renameVariableInStmts(Catch_ $catch, string $oldName, string $newName) : void { $this->traverseNodesWithCallable( $catch->stmts, function (Node $node) use($oldName, $newName) { if (!$node instanceof Variable) { return null; } if (!$this->nodeNameResolver->isName($node, $oldName)) { return null; } $node->name = $newName; return null; } ); traverseNodesWithCallable() } catch (SomeException $typoException) { $typoException->getTraceAsString(); $typoException->getMessage(); } 124 ※紙面の都合でアレンジしています

Slide 125

Slide 125 text

MethodCall Identifier|Expr Expr(Variable) $typoException->getMessage(); typoException getMessage array null 125

Slide 126

Slide 126 text

MethodCall Identifier|Expr Expr(Variable) $typoException->getMessage(); typoException getMessage array null ① ② ③ ④ 126

Slide 127

Slide 127 text

MethodCall Identifier|Expr Expr(Variable) $typoException->getMessage(); typoException getMessage array null 127

Slide 128

Slide 128 text

private function renameVariableInStmts(Catch_ $catch, string $oldName, string $newName) : void { $this->traverseNodesWithCallable( $catch->stmts, function (Node $node) use($oldName, $newName) { if (!$node instanceof Variable) { return null; } if (!$this->nodeNameResolver->isName($node, $oldName)) { return null; } $node->name = $newName; return null; } ); traverseNodesWithCallable() 128 ※紙面の都合でアレンジしています

Slide 129

Slide 129 text

traverseNodesWithCallable() 129 private function renameVariableInStmts(Catch_ $catch, string $oldName, string $newName) : void { $this->traverseNodesWithCallable( $catch->stmts, function (Node $node) use($oldName, $newName) { if (!$node instanceof Variable) { return null; } if (!$this->nodeNameResolver->isName($node, $oldName)) { return null; } $node->name = $newName; return null; } ); ※紙面の都合でアレンジしています

Slide 130

Slide 130 text

traverseNodesWithCallable() 130 private function renameVariableInStmts(Catch_ $catch, string $oldName, string $newName) : void { $this->traverseNodesWithCallable( $catch->stmts, function (Node $node) use($oldName, $newName) { if (!$node instanceof Variable) { return null; } if (!$this->nodeNameResolver->isName($node, $oldName)) { return null; } $node->name = $newName; return null; } ); ※紙面の都合でアレンジしています

Slide 131

Slide 131 text

◯◯したい!! 131

Slide 132

Slide 132 text

既成のクラス/メソッドの探し方 • ドキュメントに乏しく既存のルール[1]を読むのがよさそう。 • かといって全部読むのは無理。的を絞ってコードを読みたい。 1. https://github.com/rectorphp/rector/blob/main/docs/rector_rules_overview.md 132

Slide 133

Slide 133 text

ところで ルールが使用するクラスを コンストラクタやautowireで注入している 133

Slide 134

Slide 134 text

既成のクラス/メソッドの探し方 1. ルールに注入されるクラス[1]を洗い出す。 2. そのクラスとメソッド[2]の名称から、用途のあたりをつける。 3. そのクラスを使用するルール[3]を参照し、使い方を理解する。 1. https://hail-satin-3eb.notion.site/Rector-1e2cc094aafe4cdb877b8f6b4a87a630 2. https://hail-satin-3eb.notion.site/Rector-a4fd8c7a108b4a8190a1c20e8dae30bc 3. https://hail-satin-3eb.notion.site/Rector-6d95cfbaffaa4b149120030602513ae6 134

Slide 135

Slide 135 text

クラス・インターフェース 10選 クラス・インターフェース 用途超概要(詳細はコードを参照のこと) TestsNodeAnalyzer テストコードのノードに関する処理。 ReflectionResolver ScopeやReflectionProviderを使ってクラス、プロパティ、メソッド、関数に付随する情報 を導出する。 VisibilityManipulator Nodeの可視性(publicなど)を編集する。 ReflectionProvider クラス、プロパティ、メソッド、関数に付随する情報を取得する。 ArgsAnalyzer 引数(ArgやVariadicPlaceholder)に関する処理。 NodesToAddCollector ノードの追加等に関する処理。 IfManipulator ifノードに関する処理。 PhpDocTagRemover PHPDocのタグ削除に関する処理。 PhpVersionProvider PHPのバージョンに関する処理。 PhpDocTypeChanger PHPDocに記載する型を編集・生成する。 135

Slide 136

Slide 136 text

ケーススタディ(3/3) メソッドが属するクラスの情報が欲しい!! 136

Slide 137

Slide 137 text

たとえば protectedなメソッドをprivateに修正するルール (PrivatizeFinalClassMethodRector) 137

Slide 138

Slide 138 text

final class SomeClass { protected function someMethod() { } } final class SomeClass { private function someMethod() { } } 138

Slide 139

Slide 139 text

子ノードから親ノードに戻れない 139

Slide 140

Slide 140 text

attributes • parentノード • Scope • コードの位置に関する情報 などを含む。 140

Slide 141

Slide 141 text

attributes $node = $node->getAttribute(AttributeKey::PARENT_NODE); if (!$node instanceof Class_) { return null; } 実装例はPrivatizeFinalClassMethodRectorなどをご覧下さい。 141

Slide 142

Slide 142 text

まとめ • テキストではなくオブジェクトとして捉えると理解が捗る。 • ノードの理解を深めることが重要。 • Rectorは他の静的解析ツールを活用している。 • コードの読み取りと生成にPHP Parserを。 • 型の導出にPHPStanを。 • 公開された既成の仕組みを利用して効率よくルールが作成できる。 • 既成のクラス/メソッド • Scope • attributes 142

Slide 143

Slide 143 text

カスタムルールを作りたくなったら このトーク/資料を見直してみて下さい 143

Slide 144

Slide 144 text

Enjoy PHP!! 144

Slide 145

Slide 145 text

Enjoy 静的解析!! 145

Slide 146

Slide 146 text

ご清聴ありがとうございました 146

Slide 147

Slide 147 text

フィードバックを是非お願いしますmm • 資料や説明の不備 • 役立てられそうな点 • 静的解析に関する不明点・興味がある点 etc. 147