2016/12/20

この記事はPowerShell Advent Calendar 2016の20日目です。

はじめに

前々回はASTの概要について述べ、最後にAST.FindAllメソッドを使って、ASTから指定のASTノードを検索する方法について説明しました。

前回はASTを再帰的に検索して、木構造を視覚化してみました。

今回もASTを検索する話なのですが、静的解析機能を実装するためのAST Visitorを用いる方法について説明します。が、あらかじめお断りしておきますが、静的解析の実装までは今回はたどり着きません。静的解析ツールをどう作るかorどう作られているか、ということを雰囲気で味わっていただければと。

Visitorパターン

AST Visitorの説明をする前に、まず、Visitorパターンについて簡単に。

Visitorパターン[Wikipedia]というのは、オブジェクト指向言語におけるデザインパターンの1つで、対象オブジェクトを巡回する「訪問者」クラスを定義するものです。Visitorクラスでは、対象クラスごとに行う処理を、個別にvisitメソッドをオーバーロードさせることで定義します。共通のVisitor抽象クラスを継承することで、異なる機能を持ったVisitorクラスを作ることができます。

一方、処理対象クラスには、Visitorオブジェクトを引数に受け取る、acceptメソッドを定義します。acceptメソッドでは、引数として受け取ったVisitorオブジェクトのvisitメソッドを呼ぶことで、処理を実行させます。

なお、処理対象クラスが子要素クラスを持つ場合には、acceptメソッド内で、子要素クラスのacceptメソッドを呼ぶようにします。こうしておくことで、Visitorは処理対象を再帰的に巡回できるようになります。

このように処理対象クラスから、実際に処理を行う機能をVisitorクラスとして分離することで、処理対象クラスに手を加えることなく、Visitorクラスを追加して、処理内容を増やしたりすることが可能になります。

AST Visitorの呼び出し

Visitorパターンを念頭において、AST Visitorの呼び出し方を見ていきましょう。Ast抽象クラスには、以下の2つのVisitメソッドが定義されています。

説明に入る前に注意点。メソッド名は"Visit"となっていますが、Visitorパターンでいうところの"accept"メソッドのことです。なぜメソッド名がAcceptじゃないのかは不明ですが…。

ともかく、AstクラスのVisitメソッドは、AstVisitor抽象クラスを継承したクラスのオブジェクトか、ICustomAstVisitorインターフェースを実装したクラスのいずれかを引数に取ることで、ASTに対する処理を実施します。

AstVisitor抽象クラスを継承、もしくはICustomAstVisitorインターフェースを実装することで、ASTの種類に応じた巡回処理を行うクラスを、自分で定義していきます。

AstVisitor抽象クラス

AstVisitor抽象クラスは、Visitorとしての基本的な機能があらかじめ実装されています。具体的には既に以下の機能は用意されています。

  • ASTの種類に応じたVisitメソッドの定義
    すべての種類のASTに対応するVisitメソッド(50個以上)がVirtualメソッドとして定義されています(※)。例えば、IfStatementAstに対する処理を行うための、VisitIfStatementメソッドがあります。

    ※一般的なVisitorパターンでは、Visitメソッドを対象クラス分オーバーロードさせますが、PowerShellのAstVisitorは対象クラスに応じた別名のメソッドを定義する方式です。これも理由は分かりませんが、オーバーロードにするには多すぎるからかもしれません。

  • 子ノードの再帰的な巡回
    各Visitメソッドには、ASTの子ノードに対し、再帰的にVisitメソッドを呼ぶ仕掛けがあらかじめ備わっています。
  • ノード巡回の停止
    各VisitメソッドはAstVisitAction列挙型を返却します。以下のように返却する値によって、ノード巡回の継続、停止を制御できます。
    • Continue:ノード巡回を継続(デフォルト)
    • SkipChildren:子ノードの巡回を行わない
    • StopVisit:巡回を終了する
カスタムAstVisitorクラスを作成する

以上の基本的な機能を踏まえて、AstVisitor抽象クラスを実装したカスタムVisitorクラスを作ります。C#で書くのが一般的ですが、せっかくなのでPowerShell v5で追加された、クラス構文を使って書いてみましょう。

例えば、「利用しているコマンドのリストを取得する。ただし、コマンドのパラメータ内で別コマンドを呼び出している場合は除く。」というお題を解くことを考えます。

ASTのFindAllメソッドだと、配下に含まれるすべてのCommandAstを取得してしまうので、単純にはいきません。そこでカスタムAstVisitorクラスの出番です。

このお題を実現するVisitorクラスは以下のようになるでしょう。

using namespace System.Management.Automation.Language

class GetCommandNamesVisitor : AstVisitor
{
    [string[]]$CommandNames = @()

    [AstVisitAction]VisitCommand([CommandAst]$commandAst)
    {
        $this.CommandNames += $commandAst.CommandElements[0].Extent.Text
        return [AstVisitAction]::SkipChildren
    }
}

PowerShellのクラス構文において、Virtualメソッドのオーバーライドは、単に同名のメソッドを定義するだけですので、ここではVisitCommandメソッドをオーバーライドします。

プロパティとフィールドの区別はないので、コマンド名の一覧を格納するCommandNamesプロパティは上記のような定義になります。メソッド内でクラスメンバを参照する際には$thisを用います。

作成したGetCommandNamesVisitorクラスをインスタンス化し、解析対象スクリプトブロックのASTのVisitメソッドに引数として渡します。

$scriptBlock = {
    $files = Get-ChildItem -Path (Get-Location | Split-Path -Parent) -File
    $files | 
        Sort-Object -Property LastWriteTime -Descending | 
        Select-Object -First 5
}

$visitor = New-Object GetCommandNamesVisitor
$scriptBlock.Ast.Visit($visitor)
$visitor.CommandNames

実行すると、結果は

Get-ChildItem
Sort-Object
Select-Object

のようになるかと思います。

AstVisitorクラスの具体的な実装については、PSReadLinePowerShellEditorServicesにありますので、参考にしてみてください。

ICustomAstVisitorの実装

前項で述べた、AstVisitor抽象クラスを継承したカスタムAstVisitorクラスの場合、基本的な処理を実装する必要はないですし、目的とするASTクラスに対するVisitメソッドだけオーバーライドすればいいので、非常に簡便です。

ただ、本格的にPowerShellの構文解析を行いたい場合、ノードの巡回順だとか、その他もろもろをもっと細かく自分で実装したいケースが出てきます。

そういった場合にはICustomAstVisitorインターフェースを実装したクラスを作って対応します。ICustomAstVisitorインターフェースも、AstVisitor抽象クラス同様、各ASTクラスに応じたVisitメソッドが定義されているのですが、各VisitメソッドはAstVisitAction列挙体ではなく、object型のオブジェクトを返します。つまり、自分で好きなオブジェクトを返すように定義できるわけです。

Ast.Visit(ICustomAstVisitor)はAstVisitor抽象クラスを引数に取る場合と異なり、objectを返却するのですが、このとき返却されるのは、最初に実行されたVisitメソッドの戻り値になります。

ICustomAstVisitorはインターフェースですので、処理はすべて自分で定義しなくてはなりません(※)。ノードの再帰的探索も、必要ならもちろん自前で実装する必要があります(前回紹介した、JSON化スクリプトのような処理になるかと思います)。

※ISEだとインターフェースの実装を一発で行うリファクタリング機能はないので、今回みたく実装すべきメンバがたくさんある場合は、こんな感じのひな形を作るスクリプトを使うと良いでしょう。

今回はICustomAstVisitorインターフェースを実装したクラスの実例まではご紹介できませんでしたが、興味のある方は、PSScriptAnalyzerで用いられているので参考にしてみてください。

まとめ

PowerShellのASTについてきちんと解説している記事が英語圏を含めてもあまりないようでしたので、3回に渡って、一通りの基礎知識をまとめてみました。

普通にPowerShellを使っている分には、滅多に使うことはないと思いますが、たとえばPSScriptAnalyzerカスタムルールを自分で作る場合には、ASTの知識は必須になってきますので、必要に応じて参考にしていただければ幸いです。

2016/12/15

この記事はPowerShell Advent Calendar 2016の15日目です。

前回はPowerShellのASTの概要を解説しました。今回は前回の補足というか応用的な内容になります。

前回、スクリプトブロックからどのようなASTが生成されるのか、図で書きました。そもそもあの図を作るにあたって、ASTの構造を視覚的に把握したかったので、そのためのスクリプトを書きました。

PowerShellで木構造を展開表示する方法は色々ある(※)かと思いますが、今回はJSONとして出力して、表示については他のアプリに任せることにしました。

※Format-Customのデフォルトビューは意外と使える

ただし、ASTオブジェクトをそのままConvertTo-Jsonコマンドレットに渡すわけにはいきません。というのも、AST構造を再帰的に展開するには、探索の深さ(-Depth)を大きくしなければいけませんが、そうするとASTではないオブジェクトも逐一展開してしまい、現実的な時間内で終わらなくなってしまいます。

そこで、ASTオブジェクトそのものをJSONにするのではなく、必要なプロパティのみ再帰的に取得したカスタムオブジェクトを生成し、それをJSONにする方針を取りました。その成果が以下のコードです。(using namespace節を使っているので、v5以上必須です。)

using namespace System.Management.Automation.Language

function GetAstInner
{
    param([Ast]$ast)
    end
    {
        $base = [ordered]@{
            ExtentText = $ast.Extent.Text
            AstName = $ast.GetType().Name
        }

        $children = [ordered]@{}
        $leaves = [ordered]@{}

        $ast.psobject.Properties |
        ? Name -notin Extent, Parent |
        %{
            $type = [type]($_.TypeNameOfValue)
            $propValue = $ast.($_.Name)
                
            if($type.IsSubclassOf([ast]))
            {             
                if($null -ne $propValue)
                {
                    $children[$_.name] = GetAstInner $propValue
                }
            }
            elseif($type.IsGenericType -and $null -ne ($type.GetGenericArguments() | where{$_.Name -eq "Tuple``2"}))
            {
                $asts = @()

                foreach($next in $propValue)
                {
                    if($null -ne $next)
                    {
                        $asts += [pscustomobject]@{
                            Item1 = $(
                                if($null -ne $next.Item1 -and $next.Item1 -is [ast])
                                {
                                    GetAstInner $next.Item1
                                }
                            )
                            Item2 = $(
                                if($null -ne $next.Item2 -and $next.Item2 -is [ast])
                                {
                                    GetAstInner $next.Item2
                                }
                            )
                        }
                    }
                }

                if($asts.length -ne 0)
                {
                    $children[$_.Name] = $asts
                }

            }
            elseif($type.IsGenericType -and $null -ne ($type.GetGenericArguments() | where{$_.IsSubclassOf([ast])}) )
            {
                $asts = @()

                foreach($next in $propValue)
                {
                    if($null -ne $next)
                    {
                        $asts += GetAstInner $next
                    }
                }

                if($asts.length -ne 0)
                {
                    $children[$_.Name] = $asts
                }
            }
            else
            {
                if($null -ne $propValue)
                {
                    $leaves[$_.Name] += $propValue.Tostring()
                }
            }
        }
        [pscustomobject]($base + $leaves + $children)
    }
}

function Get-Ast
{
    param([scriptblock]$ScriptBlock)
    end
    {
        GetAstInner $ScriptBlock.Ast
    }
}

本来なら、50種以上あるAstクラスに応じてきちんと場合分けすべきなのですが、コードが長くなるだけなので、動的言語の強みを生かしてダックタイピング的な方法で下位ノードを再帰的に展開しています。

途中、IfStatementAstのClausesプロパティなどで用いられている、ReadOnlyCollection<Tuple<Ast, Ast>>型であることを確認するのに苦労してますが、多分もっといい方法があると思います…。他はAstオブジェクトそのものか、ReadOnlyCollection<Ast>を返すだけなのでそんなに苦労はないです。Ast抽象クラスに含まれているExtent、Parentプロパティ以外で、Astを要素に含まないプロパティに関しては、ASTの葉として解釈しています。

次にこのスクリプトを使って、スクリプトブロックをJSONとして出力します。

$scriptBlock = {
    param([int]$x,[int]$y)
    end
    {
        $out = $x + $y
        $out | Write-Host -ForegroundColor Red
    }
}

Get-Ast $scriptBlock | 
    ConvertTo-Json -Depth 100 |
    Set-Content ast.json

サンプルとして用いるスクリプトブロックは、前回のものと同じです。これを先ほど書いたGet-Ast関数に渡して、結果をConvertTo-JsonでJSON化しています。この際、探索の深さを100としていますが、ネストが深いスクリプトブロックなどでは、もっと大きくする必要も出てくるかもしれません。

出力されたast.jsonを、JSON Viewerを使って表示してみたのが、以下のスクリーンショットになります。

スクリーンショット 2016-12-15 09.44.06

色んなスクリプトのASTを表示して、楽しんでみてください。

ASTシリーズはもう少し続きます。次回はAST Visitorと静的解析のお話です。


Copyright © 2005-2016 Daisuke Mutaguchi All rights reserved

mailto: mutaguchi at roy.hi-ho.ne.jp

Awards

Books

Twitter