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

Paying Off Technical Dept with Rector -The First Step-

Paying Off Technical Dept with Rector -The First Step-

『Rectorと目指す 負債をためないシステム開発〜はじめの一歩〜』

PHPカンファレンス福岡 2023
2023-06-24 16:20~ VAddyホール
https://phpcon.fukuoka.jp/2023/

inouehi

June 24, 2023
Tweet

More Decks by inouehi

Other Decks in Programming

Transcript

  1. アジェンダ 1. はじめに 2. Rectorとは 3. Rectorの使い方 4. カスタムルール作成のナレッジ 1)

    メンタルモデル 2) 分析対象コードの読み込みとノードの特定 3) ルール本体の書き方 4) 既製品の活用 5. まとめ 6
  2. インストール/実行[1][2] 1. composerでインストールする。 2. rector.phpで検査対象、使用するルール等を指定する。 • initでrector.phpの雛形を生成できる。 3. 実行する。 •

    コードが修正される。 • dry runもできる。 1. https://getrector.com/documentation 2. 設定の書き方やコマンドは変わり得るので最新のドキュメントを参照のこと 14
  3. class SomeClass { public string $name; public function __construct( string

    $name = 'Dario' ) { $this->name = $name; } } Constructor Property Promotion 特徴を分析して コンストラクタの中で staticでないプロパティに 引数と同じ変数を 設定している… 21
  4. 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 修正する
  5. オブジェクトとして捉える ※簡略化しています 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
  6. Class_ Property ClassMethod Expression Assign PropertyFetch Variable Param オブジェクトとして捉える ※簡略化しています

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

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

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

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

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

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

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

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

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

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

    private string $name; public function __construct(/** Param */) = $this->name $name Param public string $name = 'Dario' 削除する 削除する 改変する 38
  18. 41

  19. class SomeClass { public string $name; public function __construct( string

    $name = 'Dario' ) { $this->name = $name; } } 43
  20. Class_ Property ClassMethod Expression Assign PropertyFetch Variable SomeClass private string

    $name; public function __construct(/** Param */) = $this->name $name Param string $name = 'Dario' 44
  21. 45

  22. public function getNodeTypes() : array { return [Class_::class]; } public

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

    function refactor(Node $node) : ?Node { // リファクタが必要かどうかを判断する // ノード(オブジェクト)を改変する } 分析対象ノードの特定 Class_ノードを分析対象とする Class_ノードを受け取る 54
  24. ノードについて理解する • ノードの種類 … 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
  25. コードの読み取り[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
  26. コードの読み取り $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
  27. コードの読み取り $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
  28. コードの読み取り $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
  29. 64

  30. コードの読み取り 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
  31. コードの読み取り 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
  32. コードの読み取り 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
  33. コードの読み取り 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
  34. コードの読み取り 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
  35. コードの読み取り 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
  36. 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
  37. 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
  38. 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
  39. 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
  40. ルールクラスの成り立ち 1. RectorのルールはAbstractRectorを拡張する。 2. AbstractRectorはNodeVisitorAbstractを拡張する。 3. NodeVisitorAbstractはbeforeTraverse(), enterNode(), leaveNode(), afterTraverse()を持つ。

    4. AbstractRectorはenterNode()の中で、前述したgetNodeTypes()やrefactor() を呼び出す。 5. RectorのルールはgetNodeTypes()やrefactor()を実装する。 81
  41. 公式サンプルの事例 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
  42. 公式サンプルの事例 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
  43. 公式サンプルの事例 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
  44. 公式サンプルの事例 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
  45. 公式サンプルの事例 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
  46. 既成のメソッド 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
  47. AbstractRector final class MyFirstRector extends AbstractRector { public function getNodeTypes():

    array {} public function refactor(Node $node): ?Node {} public function getRuleDefinition(): RuleDefinition {} } 93
  48. AbstractRector • isName() • isNames() • getName() • isObjectType() •

    getType() • traverseNodesWithCallable() • mirrorComments() • appendArgs() • removeNode() 94
  49. class SomeClass { const HI = 'hi'; } class SomeClass

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

    { /** * @var string */ const HI = 'hi'; } 98
  51. 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
  52. 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
  53. 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
  54. 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
  55. 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
  56. 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
  57. 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
  58. 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
  59. 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
  60. 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
  61. 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
  62. 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
  63. 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 ※紙面の都合でアレンジしています
  64. 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 ※紙面の都合でアレンジしています
  65. 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 ※紙面の都合でアレンジしています
  66. 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 ※紙面の都合でアレンジしています
  67. 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; } ); ※紙面の都合でアレンジしています
  68. 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; } ); ※紙面の都合でアレンジしています
  69. クラス・インターフェース 10選 クラス・インターフェース 用途超概要(詳細はコードを参照のこと) TestsNodeAnalyzer テストコードのノードに関する処理。 ReflectionResolver ScopeやReflectionProviderを使ってクラス、プロパティ、メソッド、関数に付随する情報 を導出する。 VisibilityManipulator

    Nodeの可視性(publicなど)を編集する。 ReflectionProvider クラス、プロパティ、メソッド、関数に付随する情報を取得する。 ArgsAnalyzer 引数(ArgやVariadicPlaceholder)に関する処理。 NodesToAddCollector ノードの追加等に関する処理。 IfManipulator ifノードに関する処理。 PhpDocTagRemover PHPDocのタグ削除に関する処理。 PhpVersionProvider PHPのバージョンに関する処理。 PhpDocTypeChanger PHPDocに記載する型を編集・生成する。 135
  70. final class SomeClass { protected function someMethod() { } }

    final class SomeClass { private function someMethod() { } } 138
  71. attributes $node = $node->getAttribute(AttributeKey::PARENT_NODE); if (!$node instanceof Class_) { return

    null; } 実装例はPrivatizeFinalClassMethodRectorなどをご覧下さい。 141
  72. まとめ • テキストではなくオブジェクトとして捉えると理解が捗る。 • ノードの理解を深めることが重要。 • Rectorは他の静的解析ツールを活用している。 • コードの読み取りと生成にPHP Parserを。

    • 型の導出にPHPStanを。 • 公開された既成の仕組みを利用して効率よくルールが作成できる。 • 既成のクラス/メソッド • Scope • attributes 142