$30 off During Our Annual Pro Sale. View Details »

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. Psalmで"完全に理解した"静的解析 2022/9/25 PHP Conference Japan 2022

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

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

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

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

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

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

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

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

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

  11. Psalm超概要 1. PHP Parserを使って取得したASTを用いて解析する。 2. スキャン工程(Scanning)と分析工程(Analysis)がある。 11 • PHPの構成要素を1つずつ読み込んで処理できるように加工する。 •

    構成要素を読み込みながらチェックに必要な情報を記録する。 • 構成要素を1つずつ読み込み、要素に応じたチェックを行う。
  12. AST(抽象構文木) “通常の構文木(具象構文木あるいは解析木とも言う)から、言語の意味に 関係ない情報を取り除き、意味に関係ある情報のみを取り出した(抽象し た)木構造の木である。” (Wikipedia) “構文木(こうぶんぎ)とは、構文解析の経過や結果(またはそれら両方)を 木構造で表したもの。” (Wikipedia) 12

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

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

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

    * x + 3 1 2 親 子 子 親 子 子
  16. 文と式 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
  17. 文と式 17 statement と expression

  18. PHP ParserにおけるAST <?php echo 'あしたっていまさ'; 18

  19. <?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) "'あしたっていまさ'" } } } (略) } }
  20. <?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) "'あしたっていまさ'" } } } (略) } }
  21. <?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) "'あしたっていまさ'" } } } (略) } }
  22. 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'; } }
  23. 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); }
  24. PHP ParserにおけるAST <?php $x = 1 + 2; 24

  25. PHP ParserにおけるAST <?php $x = 1 + 2; 25 =

    + $x 1 2
  26. 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
  27. 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);
  28. PHP Parser -traverse ASTを走査するには、node traverserとnode visitorを使う。 (Walking the AST) 28

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

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

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

  33. 処理の要点 33

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

  35. 前提 コードベースの規模、複雑さは以下のような感じ。 (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
  36. Codebase 型チェック処理の全体像 36 ※要点を掴むためにポイントを絞って記載しています。 各種Comparator 各種Analyzer FileAnalyzer Analyzer Populator Scanner

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

    FileScanner StatementsProvider ReflectorVisitor ProjectAnalyzer Psalm 各種Storage IssueBuffer スキャン工程 分析工程
  38. エントリスクリプト $ ./psalm /path/to/target で解析するので、psalmから見てゆく。 38

  39. エントリスクリプト #!/usr/bin/env php <?php use Psalm\Internal\Cli\Psalm; require_once __DIR__ . '/src/Psalm/Internal/Cli/Psalm.php';

    Psalm::run($argv); // 処理の入口 39
  40. Codebase 40 各種Analyzer FileAnalyzer Analyzer Populator Scanner FileScanner StatementsProvider ReflectorVisitor

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

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

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

    ProjectAnalyzer Psalm 各種Storage IssueBuffer スキャン工程 分析工程 FileScanner StatementsProvider 各種Comparator スキャン工程
  47. 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に格納する。
  48. Codebase 48 各種Analyzer FileAnalyzer Analyzer Populator Scanner FileScanner StatementsProvider ReflectorVisitor

    ProjectAnalyzer Psalm 各種Storage IssueBuffer スキャン工程 分析工程 FileScanner ReflectorVisitor 各種Storage 各種Comparator スキャン工程
  49. 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(後述)に結果を格納する。
  50. Storage 50 スキャン工程の結果を保持する領域。 スキャン工程

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

    • FunctionLikeStorage…関数等の情報を格納する スキャン工程
  52. FileStorage, ClassLikeStorage, FunctionLikeStorage 52 • FileStorage…スキャンしたファイルの情報を格納する • ClassLikeStorage…クラス等の情報を格納する • FunctionLikeStorage…関数等の情報を格納する

    FileStorageはFunctionStorageを内包し、ClassLikeStorageはMethodStorageを 内包する。それぞれFunctionLikeStorageを拡張している。 スキャン工程
  53. ReflectorVisitor - ASTのtraverse 53 NodeVisitor(参考スライド)の実装としてReflectorVisitor等を使用している。 スキャン工程 class ReflectorVisitor extends PhpParser\NodeVisitorAbstract

    implements FileSource { (略) public function enterNode(PhpParser\Node $node): ?int {
  54. 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の更新を可能にしている。 スキャン工程
  55. Codebase 55 各種Analyzer FileAnalyzer Analyzer Populator Scanner FileScanner StatementsProvider ReflectorVisitor

    ProjectAnalyzer Psalm 各種Storage IssueBuffer スキャン工程 分析工程 各種Storage Populator 各種Comparator スキャン工程
  56. Populator 56 スキャン工程の最後に、必要な情報を収集して FileStorageやClassLikeStorageを完成させる。 スキャン工程

  57. 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 {} } スキャン工程
  58. Codebase 58 各種Analyzer FileAnalyzer Analyzer Populator Scanner FileScanner StatementsProvider ReflectorVisitor

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

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

  62. Codebase 62 各種Analyzer FileAnalyzer Analyzer Populator Scanner FileScanner StatementsProvider ReflectorVisitor

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

    'meaningless'; return $s; } f(); f.php
  66. <?php function f(): string { $s = 'meaningless'; return $s;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    ReflectorVisitor ProjectAnalyzer Psalm 各種Storage IssueBuffer スキャン工程 分析工程 IssueBuffer 分析工程
  88. 分析結果 88 分析工程 • エラーを検出した場合、IssueBufferに記録して処理を続ける。 • 分析が完了したら、IssueBufferに記録したエラーを出力する。

  89. 参考: 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
  90. 参考: 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; }
  91. 参考: 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
  92. ケーススタディ 92

  93. 戻り値の型違反 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();
  94. 戻り値の型違反 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(); 検出の流れを確認する
  95. InvalidReturnStatementの検出 95 <?php function string(): string { return ''; }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    function int(): int { return string(); } ・string()の戻り値はstring型 ・int()の戻り値はint型 FunctionStorage ReturnAnalyzer IssueBuffer エラーを記録 ・string()の戻り値はstring型 ・int()の戻り値はint型 メモリ 分析工程
  112. 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
  113. 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; }
  114. 補足事項 114

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

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

  117. 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がある。
  118. 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' );
  119. 試してみる 119 <?php trim(3, 'freeze'); ERROR: InvalidScalarArgument - 略 -

    Argument 1 of trim expects string, but 3 provided (see https://psalm.dev/012) trim(3, 'freeze');
  120. [コールスタック] 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
  121. 試してみる 121 <?php trim(3, 'freeze'); • スキャン工程でstubを読み込んで、FileStorageとFunctionStorageを生成 する。(引数や戻り値の型等を保持する) • 分析工程で、Storageに保持した引数の型と、trimに与えた引数の型が

    整合的かどうかチェックする。 ERROR: InvalidScalarArgument - 略 - Argument 1 of trim expects string, but 3 provided (see https://psalm.dev/012) trim(3, 'freeze');
  122. ケースバイケースで型が変わる場合① 122 /** * @psalm-pure * * @return ($as_float is

    true ? float : string) */ function microtime(bool $as_float = false) {} 条件分岐させることができる。 前述のstub(CoreGenericFunctions.phpstub)から引用
  123. ケースバイケースで型が変わる場合② 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)から引用
  124. Psalmがサポートする型 124 PHPがサポートする型よりも細かく定義されている。 例えば、arrayはnon-empty-arrayやlistなどのように、stringはliteral-stringや numeric-stringなどのように細かく定義されている。 (Atomic types)

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

  128. 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' );
  129. plugin 129 pluginは前述したstubの他、独自ルールを定義するために利用する。 pluginの実装方法も示されている。 (Authoring Plugins)

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

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

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

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

    4. analyzerを実装する。 ※全てではなく、必要なものを実装すればよい。 FunctionCasingCheckerは1を、 plugin-phpunitは1と2を実装している。
  134. plugin - Psalm API 134 イベントハンドラのインタフェースを実装すると、所定のタイミングで実行される。

  135. 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; }
  136. 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); }
  137. plugin - composer-based plugins 137 # 開発者目線 • composer-based pluginsを作るための雛形が提供されている。

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

    pluginを使うには、psalm.xmlに登録する。
  139. plugin - psalm.xml 139 <plugins> <plugin filename="examples/plugins/FunctionCasingChecker.php"/> <pluginClass class="Psalm\PhpUnitPlugin\Plugin"/> </plugins>

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

    pluginを使うには、psalm.xmlに登録する。 composer-based-pluginを有効化すると 自動的に登録される。
  141. まとめ • Psalmの構造の概要について説明した。 • 簡単な事例を用いてPsalmの挙動、内部処理を確認した。 • stubや柔軟な型宣言の仕組みについて説明した。 • pluginの作り方、使い方の概要を説明した。 141

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

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

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

  145. Xdebug[1]を使う場合 $ PSALM_ALLOW_XDEBUG=1 ./psalm /path/to/target • コードが込み入ってくると迷子になりがち。 • 考えることに集中したい →

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

    変数を脳に置きたくない。 ⇒機械に委譲する。 146 1. ステップ実行機能等を持つデバッグツール。 https://xdebug.org/
  147. 更に脳内メモリを確保したい • 思考過程やメモをコード中に書きながら読み進める。 ⇒コードの外に書こうとすると、メモと該当箇所の紐付けが煩わしい。 • 確認したいことをどこかに箇条書きしながら読み進める。 ⇒読んでいる内に目的を忘れるのを防ぐ。読むのに集中できる。 147

  148. キャッシュを使わない[1]場合 $ ./psalm --no-cache /path/to/target (詳しくは $ ./psalm --help を参照のこと)

    148 1. Xdebugで観察する際、キャッシュの有無で処理パスが変わるのを避けたい。
  149. 簡単化する • シンプルな処理で挙動を確認する。 • 徐々に複雑にしながら、挙動の差異に着目して処理を追う。 149

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