Goの標準パッケージではじめる静的解析入門②解析編

· 1840 words · 4 minute read

はじめに

前回は用語の確認をしました。

今回は実際に流れに沿って静的解析をやってみます。


参考


静的解析の流れ

次のようなステップです。

  1. ソースコードを 字句解析 して、トークンに分割する 
  2. トークンを 構文解析 して、抽象構文木(AST)に変換する

また、構文解析では対処できない場合は、さらに意味解析や型解析を行って情報を抽出することもあります。


字句解析で使うパッケージ

字句解析

(画像はSlide Share | マスター・オブ・goパッケージ P19より引用)


ソースコードをトークンとして分割する

scanner/ScanのExampleをもとに、ソースコードを字句解析し、トークンをつくってみます。

なお、処理を見やすくするためにエラーを潰しているところがあります。

package main

import (
    "fmt"
    "go/scanner"
    "go/token"
)

func main() {
    // 字句解析するソースコード
    src := []byte("v + 1")

    // スキャナーの初期化を行う
    var s scanner.Scanner

    // ファイルの位置や長さに関する情報を保持する構造体(token.FileSet型)を用意する ※ 補足参照
    fset := token.NewFileSet()
    // ファイルをつくり、必要な値を与える
    file := fset.AddFile("", fset.Base(), len(src))

    // ソースコードをトークンにするための処理を行う(ここでファイルを使う)※ 補足参照
    s.Init(file, src, nil, scanner.ScanComments)

    for {
        // トークンを読み出す
        pos, tok, lit := s.Scan()
        if tok == token.EOF {
            break
        }

    fmt.Printf("%s\t%s\t%q\n", fset.Position(pos), tok, lit)
}

playground上で実行する

scanner/Scanはソースコードを読み込み、トークンの位置、トークン、srcで与えた値の文字列リテラルを返します。 また、この処理をEOF(ファイルの終端)に達するまで繰り返し行います。

文字列リテラルとは、ざっくりいうと、ソースコード内に値を直接表記されている0文字以上の連続した文字列の塊のことです。

出力結果です。

1:1 IDENT   "v"
1:3 +   ""
1:5 INT "1"
1:6 ;   "\n"

行数、行の中の位置、トークン、文字列リテラルが出力されました。


補足: token.FileSetについて

ここで、token.NewFileSet()という部分について解説します。

ソースコードの解析にファイルやファイルシステムは関係ないのではないか?と思うかもしれません。わたしも疑問に思いました。

上の処理でファイルは、ソースコードの解析の中で必要となるトークンの位置を取得する目的でつくっています。

トークンの位置は、scanner/Scanの処理部分で使っています。

scanner/Scanの戻り値は下記の3つです。

func (s *Scanner) Scan() (pos token.Pos, tok token.Token, lit string)

このうち、 posが位置に関係するものです。

トークンの位置情報は絶対的なものではなく、token.FileSetを基準にして相対的に決まります。

また、token.FileSetにおける「ファイル」というのは概念上のもので、ファイルシステム上に存在する必要はありません。

トークンの位置を図るための元になる値を与えるために、便宜的に「ファイル」をつくっているのです。



構文解析で使うパッケージ

構文解析

(画像はSlide Share | マスター・オブ・goパッケージ P20より引用)


ソースコードから抽象構文木(AST)を抽出する

先ほどつくったトークンを構文解析して、抽象構文木(AST)に変換してみます…

と行きたいところですが、 じつは go/parserパッケージを使うことでソースコードを字句解析して抽象構文木(AST)へ変換するところまで一気に行うことができます。

package main

import (
    "go/ast"
    "go/parser"
)

func main() {
    src := "v + 1"

    // ソースコードを抽象構文木(AST)に変換する
    expr, _ := parser.ParseExpr(src)

    // 抽象構文木(AST)をフォーマットしてプリントする
    ast.Print(nil, expr)
}

playground上で実行する

ast.Print は抽象構文木を人間に読みやすい形でプリントします。デバッグ時に便利な関数です。

出力結果を見てみます。

     0  *ast.BinaryExpr {
     1  .  X: *ast.Ident {
     2  .  .  NamePos: 1
     3  .  .  Name: "v"
     4  .  .  Obj: *ast.Object {
     5  .  .  .  Kind: bad
     6  .  .  .  Name: ""
     7  .  .  }
     8  .  }
     9  .  OpPos: 3
    10  .  Op: +
    11  .  Y: *ast.BasicLit {
    12  .  .  ValuePos: 5
    13  .  .  Kind: INT
    14  .  .  Value: "1"
    15  .  }
    16  }

いきなり抽象構文木を手に入れることができました。

また、 parser/ParseFileを使うことでソースコードではなくファイルを対象にすることもできます。

func ParseFile(fset *token.FileSet, filename string, src interface{}, mode Mode) (f *ast.File, err error)

parser/ParseFileのExample


おわりに

以上で静的解析を行う際の流れを確認しました。

抽象構文木(AST)は取得できたけれども、いまはまだ何がうれしいのかよくわかりません。

そこで次回は抽象構文木(AST)を操作してトラバースしたりファイルに出力したりしてみます。