Slide 1

Slide 1 text

Psalmで"完全に理解した"静的解析 2022/9/25 PHP Conference Japan 2022

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

コードリーディングの経験を踏まえた Psalm(静的解析)入門です。 トーク概要 3 https://fortee.jp/phpcon-2022/proposal/0c110d95-4068-4854-a190-18cc6f6b6555

Slide 4

Slide 4 text

誰向けのトーク? 1. 静的解析の仕組みに興味がある方 2. 静的解析のルールを作ってみたい方 3. PHPStanやPhanは知っているが、Psalmって何かね?という方 4

Slide 5

Slide 5 text

誰向けのトーク? 1. 静的解析の仕組みに興味がある方 2. 静的解析のルールを作ってみたい方 3. PHPStanやPhanは知っているが、Psalmって何かね?という方 5

Slide 6

Slide 6 text

本トークの目指すところ 1. 静的解析の一事例であるPsalmがどのようにコードを解析している のか、その原理を少し理解して喜ぶ。 2. これからPsalmにコントリビュートしたい方や拡張を作りたい方の ハードルを下げる。 3. PHPStanやPhanをメンテナンスする方がPsalmの仕組みを参考にで きるかもしれません。 6

Slide 7

Slide 7 text

アジェンダ 1. 基礎知識 • Psalmの処理工程 • AST • PHP Parser 2. 処理の要点 • 型チェック処理の流れ • 分析の流れ 3. ケーススタディ • 戻り値の型違反 7 4. 補足事項 • stub • ReturnTypeProvider • CallMap • Plugin 5. おまけ • コードリーディングの豆知識

Slide 8

Slide 8 text

基礎知識 8

Slide 9

Slide 9 text

Psalm超概要 Psalmは静的解析ツールである。 静的解析ツールはコードを実行することなしに不具合を見つけてくれる、実行する までエラーに気づけないPHPにおいて欠かせない存在。 9

Slide 10

Slide 10 text

Psalm超概要 1. PHP Parserを使って取得したASTを用いて解析する。 2. スキャン工程(Scanning)と分析工程(Analysis)がある。 10

Slide 11

Slide 11 text

Psalm超概要 1. PHP Parserを使って取得したASTを用いて解析する。 2. スキャン工程(Scanning)と分析工程(Analysis)がある。 11 • PHPの構成要素を1つずつ読み込んで処理できるように加工する。 • 構成要素を読み込みながらチェックに必要な情報を記録する。 • 構成要素を1つずつ読み込み、要素に応じたチェックを行う。

Slide 12

Slide 12 text

AST(抽象構文木) “通常の構文木(具象構文木あるいは解析木とも言う)から、言語の意味に 関係ない情報を取り除き、意味に関係ある情報のみを取り出した(抽象し た)木構造の木である。” (Wikipedia) “構文木(こうぶんぎ)とは、構文解析の経過や結果(またはそれら両方)を 木構造で表したもの。” (Wikipedia) 12

Slide 13

Slide 13 text

AST(抽象構文木) 13 x = (1 + 2) * 3 = * x + 3 1 2

Slide 14

Slide 14 text

AST(抽象構文木) 14 x = (1 + 2) * 3 = * x + 3 1 2 ノード ノード ノード

Slide 15

Slide 15 text

AST(抽象構文木) 15 x = (1 + 2) * 3 = * x + 3 1 2 親 子 子 親 子 子

Slide 16

Slide 16 text

文と式 16 PHPのコードは文(と式)から成る。 • 文[1] • ;で区切られた塊 • {}で囲まれた制御構造(if文等) • 式[2] • 値があるもの全て 1. https://www.php.net/manual/ja/control-structures.intro.php 2. https://www.php.net/manual/ja/language.expressions.php

Slide 17

Slide 17 text

文と式 17 statement と expression

Slide 18

Slide 18 text

PHP ParserにおけるAST

Slide 19

Slide 19 text

object(PhpParser\Node\Stmt\Echo_)#1180 (2) { ["exprs"]=> array(1) { [0]=> object(PhpParser\Node\Scalar\String_)#1179 (2) { ["value"]=> string(9) "あしたっていまさ" ["attributes":protected]=> array(4) { ["startLine"]=> int(2) ["endLine"]=> int(2) ["kind"]=> int(1) ["rawValue"]=> string(11) "'あしたっていまさ'" } } } (略) } }

Slide 20

Slide 20 text

object(PhpParser\Node\Stmt\Echo_)#1180 (2) { ["exprs"]=> array(1) { [0]=> object(PhpParser\Node\Scalar\String_)#1179 (2) { ["value"]=> string(9) "あしたっていまさ" ["attributes":protected]=> array(4) { ["startLine"]=> int(2) ["endLine"]=> int(2) ["kind"]=> int(1) ["rawValue"]=> string(11) "'あしたっていまさ'" } } } (略) } }

Slide 21

Slide 21 text

object(PhpParser\Node\Stmt\Echo_)#1180 (2) { ["exprs"]=> array(1) { [0]=> object(PhpParser\Node\Scalar\String_)#1179 (2) { ["value"]=> string(9) "あしたっていまさ" ["attributes":protected]=> array(4) { ["startLine"]=> int(2) ["endLine"]=> int(2) ["kind"]=> int(1) ["rawValue"]=> string(11) "'あしたっていまさ'" } } } (略) } }

Slide 22

Slide 22 text

PHP ParserにおけるAST Node(親)がSub Node(子)を持つという階層構造になっている。 22 class Echo_ extends Node\Stmt { /** @var Node\Expr[] Expressions */ public $exprs; (略) public function getSubNodeNames() : array { return ['exprs']; } public function getType() : string { return 'Stmt_Echo'; } }

Slide 23

Slide 23 text

abstract class Stmt extends NodeAbstract PHP ParserにおけるAST 23 abstract class NodeAbstract implements Node, \JsonSerializable interface Node { public function getType() : string; public function getSubNodeNames() : array; public function getLine() : int; public function getStartLine() : int; public function getEndLine() : int; public function getStartTokenPos() : int; public function getEndTokenPos() : int; public function getStartFilePos() : int; public function getEndFilePos() : int; public function getComments() : array; public function getDocComment(); public function setDocComment(Comment\Doc $docComment); public function setAttribute(string $key, $value); public function hasAttribute(string $key) : bool; public function getAttribute(string $key, $default = null); public function getAttributes() : array; public function setAttributes(array $attributes); }

Slide 24

Slide 24 text

PHP ParserにおけるAST

Slide 25

Slide 25 text

PHP ParserにおけるAST

Slide 26

Slide 26 text

array(1) { [0]=> object(PhpParser\Node\Stmt\Expression)#1184 (2) { ["expr"]=> object(PhpParser\Node\Expr\Assign)#1183 (3) { ["var"]=> object(PhpParser\Node\Expr\Variable)#1179 (2) { ["name"]=> string(3) "x" (略) } ["expr"]=> object(PhpParser\Node\Expr\BinaryOp\Plus)#1182 (3) { ["left"]=> object(PhpParser\Node\Scalar\LNumber)#1180 (2) { ["value"]=> int(1) (略) } ["right"]=> object(PhpParser\Node\Scalar\LNumber)#1181 (2) { ["value"]=> int(2) PHP ParserにおけるAST

Slide 27

Slide 27 text

PHP Parser 1. parse … PHPのコードを読み込んで、ASTを生成する。 2. traverse … ASTを読み込んで処理する。 27 $parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7); $ast = $parser->parse($code); $traverser = new NodeTraverser(); $traverser->addVisitor(new class extends NodeVisitorAbstract {}); $traverser->traverse($ast);

Slide 28

Slide 28 text

PHP Parser -traverse ASTを走査するには、node traverserとnode visitorを使う。 (Walking the AST) 28

Slide 29

Slide 29 text

PHP Parser -traverse ASTを走査するには、node traverserとnode visitorを使う。 29

Slide 30

Slide 30 text

PHP Parser -traverse 30 interface NodeVisitor { public function beforeTraverse(array $nodes); public function enterNode(Node $node); public function leaveNode(Node $node); public function afterTraverse(array $nodes); } ノードを見つけると、enterNode()が呼び出される。 一方、leaveNode()はすべての子ノードを走査した後に呼び出される。 A B C enterNode(A) enterNode(B) leaveNode(B) enterNode(C) leaveNode(C) leaveNode(A)

Slide 31

Slide 31 text

スキャン工程とは • 依存関係を特定する。 • 関数/メソッドのシグネチャ[1]や定数を取得する。 など 1. 引数、戻り値の型等 31

Slide 32

Slide 32 text

スキャン工程とは • 依存関係を特定する。 • 関数/メソッドのシグネチャや定数を取得する。 など Psalm\Internal\Codebase\Scanner でスキャンが行われる。 32

Slide 33

Slide 33 text

処理の要点 33

Slide 34

Slide 34 text

前提 $ ./psalm --version Psalm 4.x-dev@0d0a049eb2a3aa5c6a74998885548449ad3ef239 34 2022/8/16時点のコードに基づいて発表資料を作成する。

Slide 35

Slide 35 text

前提 コードベースの規模、複雑さは以下のような感じ。 (PhpMetricsでsrc以下を計測) 35 LOC Lines of code 83657 Comment lines of code 13108 Object oriented programming Classes 1006 Interface 65 Methods 2588 Lack of cohesion of methods 1.5 Coupling Average instability 0.57 Complexity Average Cyclomatic complexity by class 22.81 Average Relative system complexity 210.65 Average Difficulty 8.71

Slide 36

Slide 36 text

Codebase 型チェック処理の全体像 36 ※要点を掴むためにポイントを絞って記載しています。 各種Comparator 各種Analyzer FileAnalyzer Analyzer Populator Scanner FileScanner StatementsProvider ReflectorVisitor ProjectAnalyzer Psalm 各種Storage IssueBuffer スキャン工程 分析工程

Slide 37

Slide 37 text

Codebase 型チェック処理の全体像 37 ※要点を掴むためにポイントを絞って記載しています。 各種Comparator 各種Analyzer FileAnalyzer Analyzer Populator Scanner FileScanner StatementsProvider ReflectorVisitor ProjectAnalyzer Psalm 各種Storage IssueBuffer スキャン工程 分析工程

Slide 38

Slide 38 text

エントリスクリプト $ ./psalm /path/to/target で解析するので、psalmから見てゆく。 38

Slide 39

Slide 39 text

エントリスクリプト #!/usr/bin/env php

Slide 40

Slide 40 text

Codebase 40 各種Analyzer FileAnalyzer Analyzer Populator Scanner FileScanner StatementsProvider ReflectorVisitor ProjectAnalyzer Psalm 各種Storage IssueBuffer スキャン工程 分析工程 Psalm 各種Comparator

Slide 41

Slide 41 text

Psalm public static function run(array $argv): void { (略) if ($paths_to_check === null) { $project_analyzer->check($current_dir, $is_diff); } elseif ($paths_to_check) { $project_analyzer->checkPaths($paths_to_check); } (略) } 41 スキャンと分析の責務を持つ ProjectAnalyzer を呼び出す。

Slide 42

Slide 42 text

Codebase 42 各種Analyzer FileAnalyzer Analyzer Populator Scanner FileScanner StatementsProvider ReflectorVisitor ProjectAnalyzer Psalm 各種Storage IssueBuffer スキャン工程 分析工程 ProjectAnalyzer Codebase 各種Comparator

Slide 43

Slide 43 text

ProjectAnalyzer 43 public function checkPaths(array $paths_to_check): void { (略) $this->config->initializePlugins($this); $this->codebase->scanFiles($this->threads); // スキャン工程の入口 $this->config->visitStubFiles($this->codebase, $this->progress); $this->progress->startAnalyzingFiles(); $this->codebase->analyzer->analyzeFiles( // 分析工程の入口 $this, $this->threads, $this->codebase->alter_code, $this->codebase->find_unused_code === 'always' ); ※checkPaths()は入口の一例です。

Slide 44

Slide 44 text

Codebase 44 各種Analyzer FileAnalyzer Analyzer Populator Scanner FileScanner StatementsProvider ReflectorVisitor ProjectAnalyzer Psalm 各種Storage IssueBuffer スキャン工程 分析工程 Scanner FileScanner 各種Comparator スキャン工程

Slide 45

Slide 45 text

Scanner 45 • Scannerがスキャン工程を管理する。 vendor配下のファイルや $ ./psalm /path/to/target で 指定した解析対象のファイルを一つずつスキャンする。 • ファイルのスキャンをFileScannerに委譲する。 スキャン工程 [コールスタック] Psalm\Internal\Scanner\FileScanner->scan Psalm\Internal\Codebase\Scanner->scanFile Psalm\Internal\Codebase\Scanner->Psalm\Internal\Codebase\{closure:/略/Scanner.php:335-341} Psalm\Internal\Codebase\Scanner->scanFilePaths Psalm\Internal\Codebase\Scanner->scanFiles Psalm\Codebase->scanFiles Psalm\Internal\Analyzer\ProjectAnalyzer->checkPaths Psalm\Internal\Cli\Psalm::run {main}

Slide 46

Slide 46 text

Codebase 46 各種Analyzer FileAnalyzer Analyzer Populator Scanner FileScanner StatementsProvider ReflectorVisitor ProjectAnalyzer Psalm 各種Storage IssueBuffer スキャン工程 分析工程 FileScanner StatementsProvider 各種Comparator スキャン工程

Slide 47

Slide 47 text

FileScanner 47 [コールスタック] Psalm\Internal\Provider\StatementsProvider::parseStatements Psalm\Internal\Provider\StatementsProvider->getStatementsForFile Psalm\Internal\Scanner\FileScanner->scan Psalm\Internal\Codebase\Scanner->scanFile Psalm\Internal\Codebase\Scanner->Psalm\Internal\Codebase\{closure:/略/Scanner.php:335-341} Psalm\Internal\Codebase\Scanner->scanFilePaths Psalm\Internal\Codebase\Scanner->scanFiles Psalm\Codebase->scanFiles Psalm\Internal\Analyzer\ProjectAnalyzer->checkPaths Psalm\Internal\Cli\Psalm::run {main} スキャン工程 PHP Parserを使ってASTを抽出し、$stmtsに格納する。

Slide 48

Slide 48 text

Codebase 48 各種Analyzer FileAnalyzer Analyzer Populator Scanner FileScanner StatementsProvider ReflectorVisitor ProjectAnalyzer Psalm 各種Storage IssueBuffer スキャン工程 分析工程 FileScanner ReflectorVisitor 各種Storage 各種Comparator スキャン工程

Slide 49

Slide 49 text

FileScanner 49 [コールスタック] Psalm\Internal\Scanner\FileScanner->scan Psalm\Internal\Codebase\Scanner->scanFile Psalm\Internal\Codebase\Scanner->Psalm\Internal\Codebase\{closure:/略/Scanner.php:335-341} Psalm\Internal\Codebase\Scanner->scanFilePaths Psalm\Internal\Codebase\Scanner->scanFiles Psalm\Codebase->scanFiles Psalm\Internal\Analyzer\ProjectAnalyzer->checkPaths Psalm\Internal\Cli\Psalm::run {main} スキャン工程 抽出した$stmtsをtraverseして、Storage(後述)に結果を格納する。

Slide 50

Slide 50 text

Storage 50 スキャン工程の結果を保持する領域。 スキャン工程

Slide 51

Slide 51 text

FileStorage, ClassLikeStorage, FunctionLikeStorage ※この他にも様々な Storageがあります。 51 • FileStorage…スキャンしたファイルの情報を格納する • ClassLikeStorage…クラス等の情報を格納する • FunctionLikeStorage…関数等の情報を格納する スキャン工程

Slide 52

Slide 52 text

FileStorage, ClassLikeStorage, FunctionLikeStorage 52 • FileStorage…スキャンしたファイルの情報を格納する • ClassLikeStorage…クラス等の情報を格納する • FunctionLikeStorage…関数等の情報を格納する FileStorageはFunctionStorageを内包し、ClassLikeStorageはMethodStorageを 内包する。それぞれFunctionLikeStorageを拡張している。 スキャン工程

Slide 53

Slide 53 text

ReflectorVisitor - ASTのtraverse 53 NodeVisitor(参考スライド)の実装としてReflectorVisitor等を使用している。 スキャン工程 class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements FileSource { (略) public function enterNode(PhpParser\Node $node): ?int {

Slide 54

Slide 54 text

ReflectorVisitor 54 public function scan( Codebase $codebase, FileStorage $file_storage, bool $storage_from_cache = false, ?Progress $progress = null ): void { (略) $traverser = new NodeTraverser(); $traverser->addVisitor( new ReflectorVisitor($codebase, $this, $file_storage) ); $traverser->traverse($stmts); ReflectorVisitorは$codebaseや$file_storageを受け取る。これにより traverse中にFileStorage, ClassLikeStorageの更新を可能にしている。 スキャン工程

Slide 55

Slide 55 text

Codebase 55 各種Analyzer FileAnalyzer Analyzer Populator Scanner FileScanner StatementsProvider ReflectorVisitor ProjectAnalyzer Psalm 各種Storage IssueBuffer スキャン工程 分析工程 各種Storage Populator 各種Comparator スキャン工程

Slide 56

Slide 56 text

Populator 56 スキャン工程の最後に、必要な情報を収集して FileStorageやClassLikeStorageを完成させる。 スキャン工程

Slide 57

Slide 57 text

Populator - 継承の事例 57 1. Subクラスをスキャンした際、inherited()が読み込まれない。 (subSpecific()や親クラスの情報はClassLikeStorageに格納される。) 2. Populatorによりinherited()の情報が追加される。 class Super { public function inherited(): void {} } class Sub extends Super { // inherited()が明記されていない public function subSpecific(): void {} } スキャン工程

Slide 58

Slide 58 text

Codebase 58 各種Analyzer FileAnalyzer Analyzer Populator Scanner FileScanner StatementsProvider ReflectorVisitor ProjectAnalyzer Psalm 各種Storage IssueBuffer スキャン工程 分析工程 Analyzer 各種Comparator 分析工程

Slide 59

Slide 59 text

Analyzer • Analyzerが分析工程を管理する。 • スキャン工程でAnalyzer->files_to_analyzeに分析対象が格納されるが そこからファイルパスを取り出してFileAnalyzerに分析を委譲する。 59 分析工程 public function checkPaths(array $paths_to_check): void { (略) $this->codebase->scanFiles($this->threads); // スキャン工程の入口 (略) $this->codebase->analyzer->analyzeFiles( // 分析工程の入口 $this, $this->threads, $this->codebase->alter_code, $this->codebase->find_unused_code === 'always' );

Slide 60

Slide 60 text

Codebase 60 各種Analyzer FileAnalyzer Analyzer Populator Scanner FileScanner StatementsProvider ReflectorVisitor ProjectAnalyzer Psalm 各種Storage IssueBuffer スキャン工程 分析工程 FileAnalyzer 各種Comparator 分析工程

Slide 61

Slide 61 text

FileAnalyzer • PHP Parserを使ってファイル単位に$stmts(AST)を抽出する。 • $stmtsを各種分析クラス(〇〇Analyzer)等を使って分析する。 61 分析工程

Slide 62

Slide 62 text

Codebase 62 各種Analyzer FileAnalyzer Analyzer Populator Scanner FileScanner StatementsProvider ReflectorVisitor ProjectAnalyzer Psalm 各種Storage IssueBuffer スキャン工程 分析工程 各種Analyzer 各種Comparator 分析工程

Slide 63

Slide 63 text

分析クラス 責務が分けられ 多数のクラスがある。 63 分析工程 FunctionAnalyzer AttributesAnalyzer FileAnalyzer NamespaceAnalyzer FunctionLikeAnalyzer MethodAnalyzer CommentAnalyzer EchoAnalyzer ContinueAnalyzer UnsetAnalyzer ExpressionAnalyzer BreakAnalyzer ReturnAnalyzer BooleanNotAnalyzer PrintAnalyzer MatchAnalyzer EncapsulatedStringAnalyzer TernaryAnalyzer CastAnalyzer IncludeAnalyzer MagicConstAnalyzer CloneAnalyzer IssetAnalyzer UnaryPlusMinusAnalyzer StaticPropertyAssignmentAnalyzer ArrayAssignmentAnalyzer InstancePropertyAssignmentAnalyzer AtomicMethodCallAnalyzer ExistingAtomicMethodCallAnalyzer MethodCallProhibitionAnalyzer MethodVisibilityAnalyzer MethodCallPurityAnalyzer

Slide 64

Slide 64 text

分析クラス 64 分析工程 FunctionAnalyzer AttributesAnalyzer FileAnalyzer NamespaceAnalyzer FunctionLikeAnalyzer MethodAnalyzer CommentAnalyzer EchoAnalyzer ContinueAnalyzer UnsetAnalyzer ExpressionAnalyzer BreakAnalyzer ReturnAnalyzer BooleanNotAnalyzer PrintAnalyzer MatchAnalyzer EncapsulatedStringAnalyzer TernaryAnalyzer CastAnalyzer IncludeAnalyzer MagicConstAnalyzer CloneAnalyzer IssetAnalyzer UnaryPlusMinusAnalyzer StaticPropertyAssignmentAnalyzer ArrayAssignmentAnalyzer InstancePropertyAssignmentAnalyzer AtomicMethodCallAnalyzer ExistingAtomicMethodCallAnalyzer MethodCallProhibitionAnalyzer MethodVisibilityAnalyzer MethodCallPurityAnalyzer ArgumentAnalyzer FunctionCallAnalyzer ArgumentsAnalyzer StaticCallAnalyzer NewAnalyzer MethodCallAnalyzer AtomicStaticCallAnalyzer ExistingAtomicStaticCallAnalyzer ArrayFunctionArgumentsAnalyzer CallAnalyzer IncDecExpressionAnalyzer EmptyAnalyzer StaticPropertyFetchAnalyzer ClassConstFetchAnalyzer InstancePropertyFetchAnalyzer AtomicPropertyFetchAnalyzer ArrayFetchAnalyzer ConstFetchAnalyzer VariableFetchAnalyzer BitwiseNotAnalyzer YieldAnalyzer ArrayAnalyzer InstanceofAnalyzer ExitAnalyzer BinaryOpAnalyzer CoalesceAnalyzer ConcatAnalyzer NonComparisonOpAnalyzer OrAnalyzer AndAnalyzer ArithmeticOpAnalyzer NullsafeAnalyzer YieldFromAnalyzer EvalAnalyzer AssignmentAnalyzer StaticAnalyzer WhileAnalyzer SwitchCaseAnalyzer IfElseAnalyzer LoopAnalyzer ElseIfAnalyzer ElseAnalyzer IfAnalyzer TryAnalyzer DoAnalyzer SwitchAnalyzer ForeachAnalyzer IfConditionalAnalyzer ForAnalyzer ThrowAnalyzer GlobalAnalyzer ReturnTypeAnalyzer ClassLikeAnalyzer InterfaceAnalyzer TraitAnalyzer ClassAnalyzer ClosureAnalyzer SourceAnalyzer StatementsAnalyzer AlgebraAnalyzer ScopeAnalyzer TypeAnalyzer ProjectAnalyzer

Slide 65

Slide 65 text

分析の流れ 65 分析工程

Slide 66

Slide 66 text

Slide 67

Slide 67 text

Slide 68

Slide 68 text

Slide 69

Slide 69 text

Slide 70

Slide 70 text

Slide 71

Slide 71 text

Slide 72

Slide 72 text

Slide 73

Slide 73 text

Slide 74

Slide 74 text

Slide 75

Slide 75 text

Slide 76

Slide 76 text

Slide 77

Slide 77 text

Slide 78

Slide 78 text

Slide 79

Slide 79 text

Slide 80

Slide 80 text

Slide 81

Slide 81 text

Slide 82

Slide 82 text

Slide 83

Slide 83 text

Codebase 83 AtomicTypeComparator 各種Analyzer FileAnalyzer Analyzer Populator Scanner FileScanner StatementsProvider ReflectorVisitor ProjectAnalyzer Psalm 各種Storage IssueBuffer スキャン工程 分析工程 各種Comparator 分析工程

Slide 84

Slide 84 text

型チェック 84 型の種類に応じたチェッククラスが定義されている。 分析工程 ArrayTypeComparator.php AtomicTypeComparator.php // 後述 CallableTypeComparator.php ClassLikeStringComparator.php GenericTypeComparator.php IntegerRangeComparator.php KeyedArrayComparator.php ObjectComparator.php ScalarTypeComparator.php TypeComparisonResult.php UnionTypeComparator.php // 後述

Slide 85

Slide 85 text

AtomicTypeComparator 85 “Atomic types are the basic building block of all type information used in Psalm.” (Atomic types) 分析工程 etc.

Slide 86

Slide 86 text

UnionTypeComparator 86 “An annotation of the form Type1|Type2|Type3 is a Union Type. Type1, Type2 and Type3 are all acceptable possible types of that union type. Type1, Type2 and Type3 are each atomic types.” (Union Types) 分析工程

Slide 87

Slide 87 text

Codebase 87 各種Comparator 各種Analyzer FileAnalyzer Analyzer Populator Scanner FileScanner StatementsProvider ReflectorVisitor ProjectAnalyzer Psalm 各種Storage IssueBuffer スキャン工程 分析工程 IssueBuffer 分析工程

Slide 88

Slide 88 text

分析結果 88 分析工程 • エラーを検出した場合、IssueBufferに記録して処理を続ける。 • 分析が完了したら、IssueBufferに記録したエラーを出力する。

Slide 89

Slide 89 text

参考: Psalmの機能は型チェックだけじゃない 89

Slide 90

Slide 90 text

参考: Psalmの機能は型チェックだけじゃない 90 ERROR: UnusedParam - 略 - Param $foo is never referenced in this method (see https://psalm.dev/135) function unUsedParam(string $foo): void

Slide 91

Slide 91 text

参考: Psalmの機能は型チェックだけじゃない 91 class User { public $name; } $user = new User; $user->nane = "foo"; ERROR: UndefinedPropertyAssignment - 略 - Instance property User::$nane is not defined (see https://psalm.dev/038) $user->nane PHP9から例外を出力 するようになる Dynamic Properties[1] 1. https://wiki.php.net/rfc/deprecate_dynamic_properties

Slide 92

Slide 92 text

ケーススタディ 92

Slide 93

Slide 93 text

戻り値の型違反 93

Slide 94

Slide 94 text

戻り値の型違反 94

Slide 95

Slide 95 text

InvalidReturnStatementの検出 95

Slide 96

Slide 96 text

InvalidReturnStatementの検出 96

Slide 97

Slide 97 text

InvalidReturnStatementの検出 97

Slide 98

Slide 98 text

InvalidReturnStatementの検出 98

Slide 99

Slide 99 text

InvalidReturnStatementの検出 99

Slide 100

Slide 100 text

InvalidReturnStatementの検出 100

Slide 101

Slide 101 text

InvalidReturnStatementの検出 101

Slide 102

Slide 102 text

InvalidReturnStatementの検出 102

Slide 103

Slide 103 text

InvalidReturnStatementの検出 103

Slide 104

Slide 104 text

InvalidReturnStatementの検出 104

Slide 105

Slide 105 text

InvalidReturnStatementの検出 105

Slide 106

Slide 106 text

InvalidReturnStatementの検出 106

Slide 107

Slide 107 text

InvalidReturnStatementの検出 107

Slide 108

Slide 108 text

InvalidReturnStatementの検出 108

Slide 109

Slide 109 text

InvalidReturnStatementの検出 109

Slide 110

Slide 110 text

InvalidReturnStatementの検出 110

Slide 111

Slide 111 text

InvalidReturnStatementの検出 111

Slide 112

Slide 112 text

InvalidReturnTypeの検出 112 [コールスタック] Psalm\Internal\Analyzer\FunctionLike\ReturnTypeAnalyzer::verifyReturnType // 戻り値の型宣言が妥当かどうかをチェック Psalm\Internal\Analyzer\FunctionLikeAnalyzer->verifyReturnType Psalm\Internal\Analyzer\FunctionAnalyzer::analyzeStatement Psalm\Internal\Analyzer\StatementsAnalyzer::analyzeStatement Psalm\Internal\Analyzer\StatementsAnalyzer->analyze Psalm\Internal\Analyzer\FileAnalyzer->analyze Psalm\Internal\Codebase\Analyzer->Psalm\Internal\Codebase\{closure:/略/Analyzer.php:357-368} Psalm\Internal\Codebase\Analyzer->doAnalysis Psalm\Internal\Codebase\Analyzer->analyzeFiles // 分析工程の入口 Psalm\Internal\Analyzer\ProjectAnalyzer->checkPaths Psalm\Internal\Cli\Psalm::run {main} ERROR: InvalidReturnType - 略 - The declared return type 'int' for int is incorrect, got 'string' (see https://psalm.dev/011) function int(): int

Slide 113

Slide 113 text

ReturnTypeAnalyzer 113 • 戻り値の型宣言が妥当かどうか等をチェックする。 • ちなみにreturn文が複数あったり、様々な型になりうる変数をreturnするケー スにおいて、ありえる型を集計する機構を持っている。 function nullOrString(bool $foo): ?string { if ($foo) { return 'baz'; // ケース1 } return null; // ケース2 } function nullOrString(bool $foo): ?string { if ($foo) { $bar = 'baz'; // ケース1 } else { $bar = null; // ケース2 } return $bar; }

Slide 114

Slide 114 text

補足事項 114

Slide 115

Slide 115 text

ビルトイン関数等の取り扱い 115 スキャン工程でPHPをパースして引数や戻り値の型を認識する。 ユーザー定義関数ならばこの仕組みで型を認識できる。 ではPHPで書かれていないビルトイン関数等はどうするか?

Slide 116

Slide 116 text

ビルトイン関数等の取り扱い 116 スキャン工程でPHPをパースして引数や戻り値の型を認識する。 ユーザー定義関数ならばこの仕組みで型を認識できる。 ではPHPで書かれていないビルトイン関数等はどうするか? ⇒stubやCallMapによりPsalmに型を認識させる。

Slide 117

Slide 117 text

CoreGenericFunctions.phpstub 117 /** * @psalm-pure * * @psalm-flow ($string) -> return */ function trim(string $string, string $characters = " \t\n\r\0\x0B") : string {} stubsディレクトリに様々なstubが定義されており、functionの他にもclassなど のstubがある。

Slide 118

Slide 118 text

stubの読み込み 118 public function checkPaths(array $paths_to_check): void { (略) $this->config->initializePlugins($this); $this->codebase->scanFiles($this->threads); // スキャン工程の入口 $this->config->visitStubFiles($this->codebase, $this->progress); // stubを読み込む $this->progress->startAnalyzingFiles(); $this->codebase->analyzer->analyzeFiles( // 分析工程の入口 $this, $this->threads, $this->codebase->alter_code, $this->codebase->find_unused_code === 'always' );

Slide 119

Slide 119 text

試してみる 119

Slide 120

Slide 120 text

[コールスタック] Psalm\Internal\Analyzer\Statements\Expression\Call\ArgumentAnalyzer::verifyType // UnionTypeComparator(前述)で評価 Psalm\Internal\Analyzer\Statements\Expression\Call\ArgumentAnalyzer::checkFunctionLikeTypeMatches Psalm\Internal\Analyzer\Statements\Expression\Call\ArgumentAnalyzer::checkArgumentMatches // 引数を1つずつ分析 Psalm\Internal\Analyzer\Statements\Expression\Call\ArgumentsAnalyzer::checkArgumentsMatch // 3, ‘freeze’を評価 Psalm\Internal\Analyzer\Statements\Expression\Call\FunctionCallAnalyzer::analyze Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer::handleExpression Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer::analyze Psalm\Internal\Analyzer\StatementsAnalyzer::analyzeStatement Psalm\Internal\Analyzer\StatementsAnalyzer->analyze Psalm\Internal\Analyzer\FileAnalyzer->analyze Psalm\Internal\Codebase\Analyzer->Psalm\Internal\Codebase\{closure:/略/Analyzer.php:357-368} Psalm\Internal\Codebase\Analyzer->doAnalysis Psalm\Internal\Codebase\Analyzer->analyzeFiles Psalm\Internal\Analyzer\ProjectAnalyzer->checkPaths Psalm\Internal\Cli\Psalm::run {main} 試してみる 120

Slide 121

Slide 121 text

試してみる 121

Slide 122

Slide 122 text

ケースバイケースで型が変わる場合① 122 /** * @psalm-pure * * @return ($as_float is true ? float : string) */ function microtime(bool $as_float = false) {} 条件分岐させることができる。 前述のstub(CoreGenericFunctions.phpstub)から引用

Slide 123

Slide 123 text

ケースバイケースで型が変わる場合② 123 /** * @psalm-template T * @psalm-template TArray as array * * @param TArray $array * @param-out (TArray is non-empty-array ? non-empty-list : list) $array */ function sort(array &$array, int $flags = SORT_REGULAR): bool { } templateの仕組みを使ってGenericsのように型を宣言できる。 (Templating) 前述のstub(CoreGenericFunctions.phpstub)から引用

Slide 124

Slide 124 text

Psalmがサポートする型 124 PHPがサポートする型よりも細かく定義されている。 例えば、arrayはnon-empty-arrayやlistなどのように、stringはliteral-stringや numeric-stringなどのように細かく定義されている。 (Atomic types)

Slide 125

Slide 125 text

stubで表現しきれない場合 125 ReturnTypeProviderを実装する。 ArrayChunkReturnTypeProvider ArraySpliceReturnTypeProvider IteratorToArrayReturnTypeProvider ArrayColumnReturnTypeProvider ArrayUniqueReturnTypeProvider MinMaxReturnTypeProvider ArrayFillReturnTypeProvider ArrayValuesReturnTypeProvider MktimeReturnTypeProvider ArrayFilterReturnTypeProvider ClosureFromCallableReturnTypeProvider ParseUrlReturnTypeProvider ArrayMapReturnTypeProvider DomNodeAppendChild PdoStatementReturnTypeProvider ArrayMergeReturnTypeProvider ExplodeReturnTypeProvider PdoStatementSetFetchMode ArrayPadReturnTypeProvider FilterVarReturnTypeProvider RandReturnTypeProvider ArrayPointerAdjustmentReturnTypeProvider FirstArgStringReturnTypeProvider SimpleXmlElementAsXml ArrayPopReturnTypeProvider GetClassMethodsReturnTypeProvider StrReplaceReturnTypeProvider ArrayRandReturnTypeProvider GetObjectVarsReturnTypeProvider StrTrReturnTypeProvider ArrayReduceReturnTypeProvider HexdecReturnTypeProvider TriggerErrorReturnTypeProvider ArrayReverseReturnTypeProvider ImagickPixelColorReturnTypeProvider VersionCompareReturnTypeProvider ArraySliceReturnTypeProvider InArrayReturnTypeProvider

Slide 126

Slide 126 text

CallMap 126 "Callmap is a data file (formatted as a PHP file returning an array) that tells Psalm what arguments function/method takes and what it returns." (Altering callmaps)

Slide 127

Slide 127 text

サードパーティークラス等の取り扱い 127 フレームワークが提供するクラス等に対して、Psalmが提供する拡張 Docblockで型情報を上書きしたい場合にも、stubを活用できる。 pluginをインストールすることでstubを導入できる。 (Using Plugins)

Slide 128

Slide 128 text

pluginの読み込み 128 public function checkPaths(array $paths_to_check): void { (略) $this->config->initializePlugins($this); // pluginが提供するstubを認識する $this->codebase->scanFiles($this->threads); // スキャン工程の入口 $this->config->visitStubFiles($this->codebase, $this->progress); // stubを読み込む $this->progress->startAnalyzingFiles(); $this->codebase->analyzer->analyzeFiles( // 分析工程の入口 $this, $this->threads, $this->codebase->alter_code, $this->codebase->find_unused_code === 'always' );

Slide 129

Slide 129 text

plugin 129 pluginは前述したstubの他、独自ルールを定義するために利用する。 pluginの実装方法も示されている。 (Authoring Plugins)

Slide 130

Slide 130 text

plugin 130 Psalmをインストールした時点で事例が2つ含まれている。 • FunctionCasingChecker • plugin-phpunit

Slide 131

Slide 131 text

plugin 131 Psalmをインストールした時点で事例が2つ含まれている。 • FunctionCasingChecker • plugin-phpunit composer-based plugins と呼ばれるplugin (後述)

Slide 132

Slide 132 text

plugin 132 pluginを作るには 1. Psalm APIを実装する。 2. stubを実装する。(参考スライド) 3. scannerを実装する。 4. analyzerを実装する。

Slide 133

Slide 133 text

plugin 133 pluginを作るには 1. Psalm APIを実装する。 2. stubを実装する。(参考スライド) 3. scannerを実装する。 4. analyzerを実装する。 ※全てではなく、必要なものを実装すればよい。 FunctionCasingCheckerは1を、 plugin-phpunitは1と2を実装している。

Slide 134

Slide 134 text

plugin - Psalm API 134 イベントハンドラのインタフェースを実装すると、所定のタイミングで実行される。

Slide 135

Slide 135 text

plugin - Psalm API 135 “AfterAnalysisInterface - called after Psalm has completed its analysis. Use this hook if you want to do something with the analysis results.” (Psalm API) interface AfterAnalysisInterface { /** * Called after analysis is complete */ public static function afterAnalysis(AfterAnalysisEvent $event): void; }

Slide 136

Slide 136 text

plugin - RegistrationInterface 136 • RegistrationInterfaceはイベントハンドラ、stub、scanner、analyzerの登録 を担う。 • plugin-phpunitは、PluginクラスでRegistrationInterfaceを実装している。 public function __invoke(RegistrationInterface $psalm, SimpleXMLElement $config = null): void { (略) $psalm->addStubFile(__DIR__ . '/../stubs/Prophecy.phpstub'); class_exists(Hooks\TestCaseHandler::class, true); $psalm->registerHooksFromClass(Hooks\TestCaseHandler::class); }

Slide 137

Slide 137 text

plugin - composer-based plugins 137 # 開発者目線 • composer-based pluginsを作るための雛形が提供されている。 • PluginクラスでRegistrationInterfaceを実装する。 # 利用者目線 • Packagistに公開されており、composerでインストールできる。 • 後述するpsalm.xmlに登録して利用する。

Slide 138

Slide 138 text

plugin - psalm.xml 138 pluginを使うには、psalm.xmlに登録する。

Slide 139

Slide 139 text

plugin - psalm.xml 139 pluginを使うには、psalm.xmlに登録する。 xmlで指定する代わりに コマンドラインオプションを指定して 読み込ませることもできる。

Slide 140

Slide 140 text

plugin - psalm.xml 140 pluginを使うには、psalm.xmlに登録する。 composer-based-pluginを有効化すると 自動的に登録される。

Slide 141

Slide 141 text

まとめ • Psalmの構造の概要について説明した。 • 簡単な事例を用いてPsalmの挙動、内部処理を確認した。 • stubや柔軟な型宣言の仕組みについて説明した。 • pluginの作り方、使い方の概要を説明した。 141

Slide 142

Slide 142 text

いかがでしたでしょうか? 142

Slide 143

Slide 143 text

おまけ ~コードリーディングの豆知識~ 143

Slide 144

Slide 144 text

エントリスクリプト $ ./psalm /path/to/target で解析するので、psalmから読み始める。 144

Slide 145

Slide 145 text

Xdebug[1]を使う場合 $ PSALM_ALLOW_XDEBUG=1 ./psalm /path/to/target • コードが込み入ってくると迷子になりがち。 • 考えることに集中したい → 変数を脳に置きたくない。 145 1. ステップ実行機能等を持つデバッグツール。 https://xdebug.org/

Slide 146

Slide 146 text

Xdebug[1]を使う場合 $ PSALM_ALLOW_XDEBUG=1 ./psalm /path/to/target • コードが込み入ってくると迷子になりがち。 • 考えることに集中したい → 変数を脳に置きたくない。 ⇒機械に委譲する。 146 1. ステップ実行機能等を持つデバッグツール。 https://xdebug.org/

Slide 147

Slide 147 text

更に脳内メモリを確保したい • 思考過程やメモをコード中に書きながら読み進める。 ⇒コードの外に書こうとすると、メモと該当箇所の紐付けが煩わしい。 • 確認したいことをどこかに箇条書きしながら読み進める。 ⇒読んでいる内に目的を忘れるのを防ぐ。読むのに集中できる。 147

Slide 148

Slide 148 text

キャッシュを使わない[1]場合 $ ./psalm --no-cache /path/to/target (詳しくは $ ./psalm --help を参照のこと) 148 1. Xdebugで観察する際、キャッシュの有無で処理パスが変わるのを避けたい。

Slide 149

Slide 149 text

簡単化する • シンプルな処理で挙動を確認する。 • 徐々に複雑にしながら、挙動の差異に着目して処理を追う。 149

Slide 150

Slide 150 text

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