$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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  8. 基礎知識
    8

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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


    子 親


    View Slide

  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

    View Slide

  17. 文と式
    17
    statement と expression

    View Slide

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

    View Slide

  19. 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) "'あしたっていまさ'"
    }
    }
    }
    (略)
    }
    }

    View Slide

  20. 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) "'あしたっていまさ'"
    }
    }
    }
    (略)
    }
    }

    View Slide

  21. 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) "'あしたっていまさ'"
    }
    }
    }
    (略)
    }
    }

    View Slide

  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';
    }
    }

    View Slide

  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);
    }

    View Slide

  24. PHP ParserにおけるAST
    $x = 1 + 2;
    24

    View Slide

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

    View Slide

  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
    $x = 1 + 2;
    26
    =
    +
    $x
    1 2
    Assign
    Variable Plus
    LNumber
    LNumber
    Expression

    View Slide

  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);

    View Slide

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

    View Slide

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

    View Slide

  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)

    View Slide

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

    View Slide

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

    View Slide

  33. 処理の要点
    33

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  39. エントリスクリプト
    #!/usr/bin/env php
    use Psalm\Internal\Cli\Psalm;
    require_once __DIR__ . '/src/Psalm/Internal/Cli/Psalm.php';
    Psalm::run($argv); // 処理の入口
    39

    View Slide

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

    View Slide

  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 を呼び出す。

    View Slide

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

    View Slide

  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()は入口の一例です。

    View Slide

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

    View Slide

  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}

    View Slide

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

    View Slide

  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に格納する。

    View Slide

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

    View Slide

  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(後述)に結果を格納する。

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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の更新を可能にしている。
    スキャン工程

    View Slide

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

    View Slide

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

    View Slide

  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 {}
    }
    スキャン工程

    View Slide

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

    View Slide

  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'
    );

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  65. 分析の流れ
    65
    分析工程
    function f(): string
    {
    $s = 'meaningless';
    return $s;
    }
    f();
    f.php

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  82. function f(): string
    {
    $s = 'meaningless';
    return $s;
    }
    f();
    f.php
    分析の流れ
    82
    分析工程
    • 分析クラスが入れ子になって繰り返し呼び出される。
    • 抽象構文木のノードをたどりながら、ノードの種類に応じた
    分析クラスを使う。

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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)
    分析工程

    View Slide

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

    View Slide

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

    View Slide

  89. 参考: Psalmの機能は型チェックだけじゃない
    89
    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

    View Slide

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

    View Slide

  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

    View Slide

  92. ケーススタディ
    92

    View Slide

  93. 戻り値の型違反
    93
    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();

    View Slide

  94. 戻り値の型違反
    94
    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();
    検出の流れを確認する

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  111. InvalidReturnStatementの検出
    111
    function string(): string { return ''; }
    function int(): int
    {
    return string();
    }
    ・string()の戻り値はstring型
    ・int()の戻り値はint型
    FunctionStorage
    ReturnAnalyzer
    IssueBuffer
    エラーを記録
    ・string()の戻り値はstring型
    ・int()の戻り値はint型
    メモリ
    分析工程

    View Slide

  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

    View Slide

  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;
    }

    View Slide

  114. 補足事項
    114

    View Slide

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

    View Slide

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

    View Slide

  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がある。

    View Slide

  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'
    );

    View Slide

  119. 試してみる
    119
    trim(3, 'freeze');
    ERROR: InvalidScalarArgument - 略 - Argument 1 of trim expects string, but 3 provided
    (see https://psalm.dev/012)
    trim(3, 'freeze');

    View Slide

  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

    View Slide

  121. 試してみる
    121
    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');

    View Slide

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

    View Slide

  123. ケースバイケースで型が変わる場合②
    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)から引用

    View Slide

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

    View Slide

  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

    View Slide

  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)

    View Slide

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

    View Slide

  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'
    );

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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;
    }

    View Slide

  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);
    }

    View Slide

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

    View Slide

  138. plugin - psalm.xml
    138




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

    View Slide

  139. plugin - psalm.xml
    139




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

    View Slide

  140. plugin - psalm.xml
    140




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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide