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

Introduction to Static Analysis through Psalm

inouehi
September 25, 2022

Introduction to Static Analysis through Psalm

『Psalmで"完全に理解した"静的解析』

PHP Conference Japan 2022
2022-09-25 16:00~ Track3
https://phpcon.connpass.com/event/255324/

inouehi

September 25, 2022
Tweet

More Decks by inouehi

Other Decks in Programming

Transcript

  1. アジェンダ 1. 基礎知識 • Psalmの処理工程 • AST • PHP Parser

    2. 処理の要点 • 型チェック処理の流れ • 分析の流れ 3. ケーススタディ • 戻り値の型違反 7 4. 補足事項 • stub • ReturnTypeProvider • CallMap • Plugin 5. おまけ • コードリーディングの豆知識
  2. Psalm超概要 1. PHP Parserを使って取得したASTを用いて解析する。 2. スキャン工程(Scanning)と分析工程(Analysis)がある。 11 • PHPの構成要素を1つずつ読み込んで処理できるように加工する。 •

    構成要素を読み込みながらチェックに必要な情報を記録する。 • 構成要素を1つずつ読み込み、要素に応じたチェックを行う。
  3. AST(抽象構文木) 14 x = (1 + 2) * 3 =

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

    * x + 3 1 2 親 子 子 親 子 子
  5. 文と式 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
  6. <?php echo 'あしたっていまさ'; PHP ParserにおけるAST 19 array(1) { [0]=> 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) "'あしたっていまさ'" } } } (略) } }
  7. <?php echo 'あしたっていまさ'; PHP ParserにおけるAST 20 array(1) { [0]=> 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) "'あしたっていまさ'" } } } (略) } }
  8. <?php echo 'あしたっていまさ'; PHP ParserにおけるAST 21 array(1) { [0]=> 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) "'あしたっていまさ'" } } } (略) } }
  9. 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'; } }
  10. 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); }
  11. 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 <?php $x = 1 + 2; 26 = + $x 1 2 Assign Variable Plus LNumber LNumber Expression
  12. 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);
  13. 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)
  14. 前提 コードベースの規模、複雑さは以下のような感じ。 (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
  15. Codebase 型チェック処理の全体像 36 ※要点を掴むためにポイントを絞って記載しています。 各種Comparator 各種Analyzer FileAnalyzer Analyzer Populator Scanner

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

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

    ProjectAnalyzer Psalm 各種Storage IssueBuffer スキャン工程 分析工程 Psalm 各種Comparator
  18. 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 を呼び出す。
  19. Codebase 42 各種Analyzer FileAnalyzer Analyzer Populator Scanner FileScanner StatementsProvider ReflectorVisitor

    ProjectAnalyzer Psalm 各種Storage IssueBuffer スキャン工程 分析工程 ProjectAnalyzer Codebase 各種Comparator
  20. 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()は入口の一例です。
  21. Codebase 44 各種Analyzer FileAnalyzer Analyzer Populator Scanner FileScanner StatementsProvider ReflectorVisitor

    ProjectAnalyzer Psalm 各種Storage IssueBuffer スキャン工程 分析工程 Scanner FileScanner 各種Comparator スキャン工程
  22. 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}
  23. Codebase 46 各種Analyzer FileAnalyzer Analyzer Populator Scanner FileScanner StatementsProvider ReflectorVisitor

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

    ProjectAnalyzer Psalm 各種Storage IssueBuffer スキャン工程 分析工程 FileScanner ReflectorVisitor 各種Storage 各種Comparator スキャン工程
  25. FileStorage, ClassLikeStorage, FunctionLikeStorage 52 • FileStorage…スキャンしたファイルの情報を格納する • ClassLikeStorage…クラス等の情報を格納する • FunctionLikeStorage…関数等の情報を格納する

    FileStorageはFunctionStorageを内包し、ClassLikeStorageはMethodStorageを 内包する。それぞれFunctionLikeStorageを拡張している。 スキャン工程
  26. 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の更新を可能にしている。 スキャン工程
  27. Codebase 55 各種Analyzer FileAnalyzer Analyzer Populator Scanner FileScanner StatementsProvider ReflectorVisitor

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

    ProjectAnalyzer Psalm 各種Storage IssueBuffer スキャン工程 分析工程 Analyzer 各種Comparator 分析工程
  29. 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' );
  30. Codebase 60 各種Analyzer FileAnalyzer Analyzer Populator Scanner FileScanner StatementsProvider ReflectorVisitor

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

    ProjectAnalyzer Psalm 各種Storage IssueBuffer スキャン工程 分析工程 各種Analyzer 各種Comparator 分析工程
  32. 分析クラス 責務が分けられ 多数のクラスがある。 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
  33. 分析クラス 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
  34. <?php function f(): string { $s = 'meaningless'; return $s;

    } f(); f.php 分析の流れ 66 分析工程 statement ※要点を掴むためにポイントを絞って記載しています。
  35. <?php function f(): string { $s = 'meaningless'; return $s;

    } f(); f.php 分析の流れ 67 分析工程 FileAnalyzer ※要点を掴むためにポイントを絞って記載しています。 statement
  36. <?php function f(): string { $s = 'meaningless'; return $s;

    } f(); f.php 分析の流れ 68 分析工程 ※要点を掴むためにポイントを絞って記載しています。 StatementsAnalyzer statement
  37. <?php function f(): string { $s = 'meaningless'; return $s;

    } f(); f.php 分析の流れ 69 分析工程 ※要点を掴むためにポイントを絞って記載しています。 FunctionAnalyzer
  38. <?php function f(): string { $s = 'meaningless'; return $s;

    } f(); f.php 分析の流れ 70 分析工程 ※要点を掴むためにポイントを絞って記載しています。 FunctionLikeAnalyzer
  39. <?php function f(): string { $s = 'meaningless'; return $s;

    } f(); f.php 分析の流れ 71 分析工程 ※要点を掴むためにポイントを絞って記載しています。 StatementsAnalyzer
  40. <?php function f(): string { $s = 'meaningless'; return $s;

    } f(); f.php 分析の流れ 72 分析工程 ※要点を掴むためにポイントを絞って記載しています。 ExpressionAnalyzer ‘’ Assign Variable String_ Expression $s =
  41. <?php function f(): string { $s = 'meaningless'; return $s;

    } f(); f.php 分析の流れ 73 分析工程 ※要点を掴むためにポイントを絞って記載しています。 AssignmentAnalyzer ‘’ Assign Variable String_ Expression $s =
  42. <?php function f(): string { $s = 'meaningless'; return $s;

    } f(); f.php 分析の流れ 74 分析工程 ※要点を掴むためにポイントを絞って記載しています。 ExpressionAnalyzer ‘’ Assign Variable String_ Expression $s =
  43. <?php function f(): string { $s = 'meaningless'; return $s;

    } f(); f.php 分析の流れ 75 分析工程 ※要点を掴むためにポイントを絞って記載しています。 StatementsAnalyzer
  44. <?php function f(): string { $s = 'meaningless'; return $s;

    } f(); f.php 分析の流れ 76 分析工程 ※要点を掴むためにポイントを絞って記載しています。 ReturnAnalyzer
  45. <?php function f(): string { $s = 'meaningless'; return $s;

    } f(); f.php 分析の流れ 77 分析工程 ※要点を掴むためにポイントを絞って記載しています。 ExpressionAnalyzer
  46. <?php function f(): string { $s = 'meaningless'; return $s;

    } f(); f.php 分析の流れ 78 分析工程 ※要点を掴むためにポイントを絞って記載しています。 VariableFetchAnalyzer
  47. <?php function f(): string { $s = 'meaningless'; return $s;

    } f(); f.php 分析の流れ 79 分析工程 ※要点を掴むためにポイントを絞って記載しています。 StatementsAnalyzer
  48. <?php function f(): string { $s = 'meaningless'; return $s;

    } f(); f.php 分析の流れ 80 分析工程 ※要点を掴むためにポイントを絞って記載しています。 ExpressionAnalyzer
  49. <?php function f(): string { $s = 'meaningless'; return $s;

    } f(); f.php 分析の流れ 81 分析工程 ※要点を掴むためにポイントを絞って記載しています。 FunctionCallAnalyzer
  50. <?php function f(): string { $s = 'meaningless'; return $s;

    } f(); f.php 分析の流れ 82 分析工程 • 分析クラスが入れ子になって繰り返し呼び出される。 • 抽象構文木のノードをたどりながら、ノードの種類に応じた 分析クラスを使う。
  51. Codebase 83 AtomicTypeComparator 各種Analyzer FileAnalyzer Analyzer Populator Scanner FileScanner StatementsProvider

    ReflectorVisitor ProjectAnalyzer Psalm 各種Storage IssueBuffer スキャン工程 分析工程 各種Comparator 分析工程
  52. 型チェック 84 型の種類に応じたチェッククラスが定義されている。 分析工程 ArrayTypeComparator.php AtomicTypeComparator.php // 後述 CallableTypeComparator.php ClassLikeStringComparator.php

    GenericTypeComparator.php IntegerRangeComparator.php KeyedArrayComparator.php ObjectComparator.php ScalarTypeComparator.php TypeComparisonResult.php UnionTypeComparator.php // 後述
  53. AtomicTypeComparator 85 “Atomic types are the basic building block of

    all type information used in Psalm.” (Atomic types) 分析工程 etc.
  54. 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) 分析工程
  55. Codebase 87 各種Comparator 各種Analyzer FileAnalyzer Analyzer Populator Scanner FileScanner StatementsProvider

    ReflectorVisitor ProjectAnalyzer Psalm 各種Storage IssueBuffer スキャン工程 分析工程 IssueBuffer 分析工程
  56. 参考: Psalmの機能は型チェックだけじゃない 89 <?php function missingParamType($foo): bool { return is_numeric($foo);

    } ERROR: MissingParamType - 略 - Parameter $foo has no provided type (see https://psalm.dev/154) function missingParamType($foo): bool
  57. 参考: Psalmの機能は型チェックだけじゃない 90 ERROR: UnusedParam - 略 - Param $foo

    is never referenced in this method (see https://psalm.dev/135) function unUsedParam(string $foo): void <?php function unusedParam(string $foo): void { return; }
  58. 参考: 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
  59. 戻り値の型違反 93 <?php function string(): string { return ''; }

    function int(): int { return string(); } 型の不一致 ERROR: InvalidReturnType - 略 - The declared return type 'int' for int is incorrect, got 'string' (see https://psalm.dev/011) function int(): int ERROR: InvalidReturnStatement - 略 - The inferred type 'string' does not match the declared return type 'int' for int (see https://psalm.dev/128) return string();
  60. 戻り値の型違反 94 <?php function string(): string { return ''; }

    function int(): int { return string(); } 型の不一致 ERROR: InvalidReturnType - 略 - The declared return type 'int' for int is incorrect, got 'string' (see https://psalm.dev/011) function int(): int ERROR: InvalidReturnStatement - 略 - The inferred type 'string' does not match the declared return type 'int' for int (see https://psalm.dev/128) return string(); 検出の流れを確認する
  61. InvalidReturnStatementの検出 95 <?php function string(): string { return ''; }

    function int(): int { return string(); } スキャン工程でStorageにスキャン結果を格納する。 スキャン工程
  62. InvalidReturnStatementの検出 96 <?php function string(): string { return ''; }

    function int(): int { return string(); } スキャン工程でStorageにスキャン結果を格納する。 ・string()の戻り値はstring型 ・int()の戻り値はint型 FunctionStorage スキャン工程
  63. InvalidReturnStatementの検出 97 <?php function string(): string { return ''; }

    function int(): int { return string(); } ・string()の戻り値はstring型 ・int()の戻り値はint型 FunctionStorage FileAnalyzer 分析工程
  64. InvalidReturnStatementの検出 98 <?php function string(): string { return ''; }

    function int(): int { return string(); } ・string()の戻り値はstring型 ・int()の戻り値はint型 FunctionStorage StatementsAnalyzer 分析工程
  65. InvalidReturnStatementの検出 99 <?php function string(): string { return ''; }

    // 割愛 function int(): int { return string(); } ・string()の戻り値はstring型 ・int()の戻り値はint型 FunctionStorage FunctionAnalyzer FunctionLikeAnalyzer 分析工程
  66. InvalidReturnStatementの検出 100 <?php function string(): string { return ''; }

    function int(): int { return string(); } ・string()の戻り値はstring型 ・int()の戻り値はint型 FunctionStorage StatementsAnalyzer 分析工程
  67. InvalidReturnStatementの検出 101 <?php function string(): string { return ''; }

    function int(): int { return string(); } ・string()の戻り値はstring型 ・int()の戻り値はint型 FunctionStorage ReturnAnalyzer 分析工程
  68. InvalidReturnStatementの検出 102 <?php function string(): string { return ''; }

    function int(): int { return string(); } ・string()の戻り値はstring型 ・int()の戻り値はint型 FunctionStorage ExpressionAnalyzer 分析工程
  69. InvalidReturnStatementの検出 103 <?php function string(): string { return ''; }

    function int(): int { return string(); } ・string()の戻り値はstring型 ・int()の戻り値はint型 FunctionStorage FunctionCallAnalyzer 分析工程
  70. InvalidReturnStatementの検出 104 <?php function string(): string { return ''; }

    function int(): int { return string(); } ・string()の戻り値はstring型 ・int()の戻り値はint型 FunctionStorage FunctionCallAnalyzer ・string()の戻り値はstring型 分析工程
  71. InvalidReturnStatementの検出 105 <?php function string(): string { return ''; }

    function int(): int { return string(); } ・string()の戻り値はstring型 ・int()の戻り値はint型 FunctionStorage FunctionCallAnalyzer ・string()の戻り値はstring型 ・string()の戻り値はstring型 メモリ 分析工程
  72. InvalidReturnStatementの検出 106 <?php function string(): string { return ''; }

    function int(): int { return string(); } ・string()の戻り値はstring型 ・int()の戻り値はint型 FunctionStorage ReturnAnalyzer ・int()の戻り値はint型 ・string()の戻り値はstring型 メモリ 分析工程
  73. InvalidReturnStatementの検出 107 <?php function string(): string { return ''; }

    function int(): int { return string(); } ・string()の戻り値はstring型 ・int()の戻り値はint型 FunctionStorage ReturnAnalyzer ・int()の戻り値はint型 ・string()の戻り値はstring型 ・int()の戻り値はint型 メモリ 分析工程
  74. InvalidReturnStatementの検出 108 <?php function string(): string { return ''; }

    function int(): int { return string(); } ・string()の戻り値はstring型 ・int()の戻り値はint型 FunctionStorage ReturnAnalyzer ・string()の戻り値はstring型 ・int()の戻り値はint型 メモリ UnionTypeComparator 分析工程
  75. InvalidReturnStatementの検出 109 <?php function string(): string { return ''; }

    function int(): int { return string(); } ・string()の戻り値はstring型 ・int()の戻り値はint型 FunctionStorage ReturnAnalyzer ・string()の戻り値はstring型 ・int()の戻り値はint型 メモリ UnionTypeComparator AtomicTypeComparator 分析工程
  76. InvalidReturnStatementの検出 110 <?php function string(): string { return ''; }

    function int(): int { return string(); } ・string()の戻り値はstring型 ・int()の戻り値はint型 FunctionStorage ReturnAnalyzer ・string()の戻り値はstring型 ・int()の戻り値はint型 メモリ UnionTypeComparator AtomicTypeComparator ScalarTypeComparator 型の不一致を認識 分析工程
  77. InvalidReturnStatementの検出 111 <?php function string(): string { return ''; }

    function int(): int { return string(); } ・string()の戻り値はstring型 ・int()の戻り値はint型 FunctionStorage ReturnAnalyzer IssueBuffer エラーを記録 ・string()の戻り値はstring型 ・int()の戻り値はint型 メモリ 分析工程
  78. 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
  79. 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; }
  80. 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がある。
  81. 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' );
  82. 試してみる 119 <?php trim(3, 'freeze'); ERROR: InvalidScalarArgument - 略 -

    Argument 1 of trim expects string, but 3 provided (see https://psalm.dev/012) trim(3, 'freeze');
  83. [コールスタック] 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
  84. ケースバイケースで型が変わる場合① 122 /** * @psalm-pure * * @return ($as_float is

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

    array<T> * * @param TArray $array * @param-out (TArray is non-empty-array ? non-empty-list<T> : list<T>) $array */ function sort(array &$array, int $flags = SORT_REGULAR): bool { } templateの仕組みを使ってGenericsのように型を宣言できる。 (Templating) 前述のstub(CoreGenericFunctions.phpstub)から引用
  86. 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
  87. 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)
  88. 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' );
  89. plugin 133 pluginを作るには 1. Psalm APIを実装する。 2. stubを実装する。(参考スライド) 3. scannerを実装する。

    4. analyzerを実装する。 ※全てではなく、必要なものを実装すればよい。 FunctionCasingCheckerは1を、 plugin-phpunitは1と2を実装している。
  90. 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; }
  91. 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); }
  92. plugin - composer-based plugins 137 # 開発者目線 • composer-based pluginsを作るための雛形が提供されている。

    • PluginクラスでRegistrationInterfaceを実装する。 # 利用者目線 • Packagistに公開されており、composerでインストールできる。 • 後述するpsalm.xmlに登録して利用する。
  93. plugin - psalm.xml 139 <plugins> <plugin filename="examples/plugins/FunctionCasingChecker.php"/> <pluginClass class="Psalm\PhpUnitPlugin\Plugin"/> </plugins>

    pluginを使うには、psalm.xmlに登録する。 xmlで指定する代わりに コマンドラインオプションを指定して 読み込ませることもできる。
  94. plugin - psalm.xml 140 <plugins> <plugin filename="examples/plugins/FunctionCasingChecker.php"/> <pluginClass class="Psalm\PhpUnitPlugin\Plugin"/> </plugins>

    pluginを使うには、psalm.xmlに登録する。 composer-based-pluginを有効化すると 自動的に登録される。
  95. Xdebug[1]を使う場合 $ PSALM_ALLOW_XDEBUG=1 ./psalm /path/to/target • コードが込み入ってくると迷子になりがち。 • 考えることに集中したい →

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

    変数を脳に置きたくない。 ⇒機械に委譲する。 146 1. ステップ実行機能等を持つデバッグツール。 https://xdebug.org/
  97. キャッシュを使わない[1]場合 $ ./psalm --no-cache /path/to/target (詳しくは $ ./psalm --help を参照のこと)

    148 1. Xdebugで観察する際、キャッシュの有無で処理パスが変わるのを避けたい。