LayerX エンジニアブログ

LayerX の エンジニアブログです。

つくってまなぶ静的解析のすすめ

はじめに

こんにちは、LayerXの id:convto です。

そしてこれは LayerX アドベントカレンダー (概念) の1日目の記事です。

アドベントカレンダー盛り上げていくぞ〜ということで11月から始まるらしいです。だいぶフライングしてるけど枠もかなり埋まっててすごい。

せっかくなのでお祭り参加したいぞ〜ということで一発目です。よろしくお願いします。

静的解析つくろうとしたきっかけ

ちょうどつい最近記事になった下記の輪読会がきっかけでした。

tech.layerx.co.jp

このなかで、mapのrange accesssについて、元mapのcopyを取らないから破壊するとループ挙動が壊れる可能性がある旨が言及されていました。

そのときは雑談で「range accessしてるmapに再代入してたら怒る!みたいな考え方で整理したら静的解析できそうっすよね〜」みたいな話をしたんですが、そういえば Go で静的解析ツール作ったことなかったなと思いちょうどいいので手を動かしてみた次第です。

Go はこのあたりのエコシステムがかなり良くできていて、標準/準標準で以下のようにかなりのパッケージがホストされています。(まだまだほかにもある)

また、go/typesについてはめちゃめちゃよいドキュメントもあります。
イメージ的にはgo/astよりさらに抽象的なAPIというかんじで、実際のASTからより利用しやすい形でいろいろ提供してくれてるやつです。

github.com

エコシステムも整っていて、かつちょうどいいモチベーションもあったので、それはやるしかないでしょうということでやってみました。

つくったもの

github.com

やりたかったこと

発想としてはmapに対してfor rangeしてるときに、ループ内で自身のmapを書き換えてるところを見つけたら怒る!という感じです。

mapのサイズが変わるようなコードでなければloopの挙動自体には影響出ないかもしれませんが、今回は簡単のために代入時点で警告を出すロジックで考えます。

典型的には以下のようなケースですね。

m := map[string]string{"a": "item a", "b": "item b"}
for k, v := range m {
    m[k] = "reassigned: " + v // want detection
}

基本は hoge[key] = val の形になってるとき hoge がrangeでぶん回してるmap自身だったら危険性のある書き込みということで警告すればよいはずです。

この書き方だけではarray/sliceへの代入か判断できないので、うまく静的解析して型情報を取り出してmapのみに限定した処理としたいです。

また、パッケージを跨いだ以下のようなパターンでも検出も行いたいので、必要があれば依存関係の解決もする必要があるのが面倒なところです。

-- go.mod --
module maptest

go 1.21

-- pkg/map.go --
package pkg

var M = map[string]string{"a": "item a", "b": "item b"}

-- main.go --
package main

import (
    "maptest/pkg"
)

func main() {
    for k, v := range pkg.M {
        pkg.M[k] = "reassigned: " + v // want detection
    }
}

x/tools/go/analysis を使って側をつくる

x/tools/go/analysis は静的解析の結果をモジュール化して依存関係を明言しながら解析処理を作ったり、 go vet に食わせられるコマンドをシュッとつくれたりするえらいやつです。

pkg.go.dev

過去の事例もめっちゃあるのでここの紹介はほどほどにしますが、以下みたいなノリでAnalizerを作れます。

package mapbreak

import (
    "golang.org/x/tools/go/analysis"
    "golang.org/x/tools/go/analysis/passes/inspect"
    "golang.org/x/tools/go/ast/inspector"
)

var Analyzer = &analysis.Analyzer{
    Name: "mapbreak",
    Doc:  Doc,
    Run:  run,
    Requires: []*analysis.Analyzer{
        inspect.Analyzer,
    },
}

const Doc = "mapbreak detects if there is map reassignment in the range access"

func run(pass *analysis.Pass) (interface{}, error) {
    inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)

    nodeFilter := []ast.Node{
        (*ast.RangeStmt)(nil),
    }

    inspect.Preorder(nodeFilter, func(n ast.Node) {
        // ここに君だけの最強の静的解析処理を書こう
    }
  }
}

あいたところに君だけの最強の処理を書けばOKです。

"golang.org/x/tools/go/analysis/passes" には便利げなAnalizerがいろいろ準備されてて、今回使ってるpasses/inspectはよしなにASTのinspectorを返してくれます。

pkg.go.dev

今回つかってる inspcet.Preorder() は特定のノード種別を指定して探索してくれるやつで、まあ静的解析やりたい時は何かしら目的があるだろうから大体これ使っとけばじゃないかという感じがあります。

go vet に渡せるコマンドはこういう感じで作れます。

package main

import (
    "github.com/convto/mapbreak"
    "golang.org/x/tools/go/analysis/singlechecker"
)

func main() { singlechecker.Main(mapbreak.Analyzer) }

main関数に突っ込んで準備されてるやつに食わせるだけです。便利!

これで以下の感じで go vet に食わせられます。

$ go vet -vettool=$(which mapbreak) ./... 

テストの準備をする

よしななテストの機能も提供されています。それが以下です。

pkg.go.dev

testdata 配下によしなに対象となるコードを配置できて、いい感じに探索してくれる優れものです。
ディレクトリ名でpatternもわけられるので、以下のようにテストが書けます。

func Test(t *testing.T) {
    testdata := analysistest.TestData()
    patterns := []string{
        "target_pattern",
    }
    for _, pattern := range patterns {
        pattern := pattern
        t.Run(pattern, func(t *testing.T) {
            t.Parallel()
            analysistest.Run(t, testdata, mapbreak.Analyzer, pattern)
        })
    }
}

また go.modがあるとgo modulesを利用する挙動 なので、パッケージをimportするようなテストも書けます。(めちゃundocumentedでもう少しよくしたい〜というコメントもあるのでそのうちやり方が変わる可能性あり)

testdata 配下にpatternに一致するようなディレクトリをつくりテストで解析対象としたい Go コードを書けばよいです。

そのさい、検出を期待する場合は // want: "message" のように書くことでテストできます。

package main

func main() {
    m := map[string]string{"a": "item a", "b": "item b"}
    for k, v := range m {
        m[k] = "reassigned: " + v // want "detected range access to map and reassigning"
    }
}

これでテストの準備は万端!ではやっていこう!

ASTを眺めて実装のイメージを固める

以下を使うと実際のCLIで実際のコードのASTが見れます。

pkg.go.dev

ためしに以下のコードで確認すると

m := map[string]string{"a": "item a", "b": "item b"}
for k, v := range m {
    m[k] = "reassigned: " + v
}

こういう感じの出力が出ます。(長いので畳みます)

gotype -ast の実行結果

$ gotype -ast singlefile.go
     0  *ast.File {
     1  .  Package: singlefile.go:1:1
     2  .  Name: *ast.Ident {
     3  .  .  NamePos: singlefile.go:1:9
     4  .  .  Name: "main"
     5  .  }
     6  .  Decls: []ast.Decl (len = 1) {
     7  .  .  0: *ast.FuncDecl {
     8  .  .  .  Name: *ast.Ident {
     9  .  .  .  .  NamePos: singlefile.go:3:6
    10  .  .  .  .  Name: "main"
    11  .  .  .  .  Obj: *ast.Object {
    12  .  .  .  .  .  Kind: func
    13  .  .  .  .  .  Name: "main"
    14  .  .  .  .  .  Decl: *(obj @ 7)
    15  .  .  .  .  }
    16  .  .  .  }
    17  .  .  .  Type: *ast.FuncType {
    18  .  .  .  .  Func: singlefile.go:3:1
    19  .  .  .  .  Params: *ast.FieldList {
    20  .  .  .  .  .  Opening: singlefile.go:3:10
    21  .  .  .  .  .  Closing: singlefile.go:3:11
    22  .  .  .  .  }
    23  .  .  .  }
    24  .  .  .  Body: *ast.BlockStmt {
    25  .  .  .  .  Lbrace: singlefile.go:3:13
    26  .  .  .  .  List: []ast.Stmt (len = 2) {
    27  .  .  .  .  .  0: *ast.AssignStmt {
    28  .  .  .  .  .  .  Lhs: []ast.Expr (len = 1) {
    29  .  .  .  .  .  .  .  0: *ast.Ident {
    30  .  .  .  .  .  .  .  .  NamePos: singlefile.go:4:2
    31  .  .  .  .  .  .  .  .  Name: "m"
    32  .  .  .  .  .  .  .  .  Obj: *ast.Object {
    33  .  .  .  .  .  .  .  .  .  Kind: var
    34  .  .  .  .  .  .  .  .  .  Name: "m"
    35  .  .  .  .  .  .  .  .  .  Decl: *(obj @ 27)
    36  .  .  .  .  .  .  .  .  }
    37  .  .  .  .  .  .  .  }
    38  .  .  .  .  .  .  }
    39  .  .  .  .  .  .  TokPos: singlefile.go:4:4
    40  .  .  .  .  .  .  Tok: :=
    41  .  .  .  .  .  .  Rhs: []ast.Expr (len = 1) {
    42  .  .  .  .  .  .  .  0: *ast.CompositeLit {
    43  .  .  .  .  .  .  .  .  Type: *ast.MapType {
    44  .  .  .  .  .  .  .  .  .  Map: singlefile.go:4:7
    45  .  .  .  .  .  .  .  .  .  Key: *ast.Ident {
    46  .  .  .  .  .  .  .  .  .  .  NamePos: singlefile.go:4:11
    47  .  .  .  .  .  .  .  .  .  .  Name: "string"
    48  .  .  .  .  .  .  .  .  .  }
    49  .  .  .  .  .  .  .  .  .  Value: *ast.Ident {
    50  .  .  .  .  .  .  .  .  .  .  NamePos: singlefile.go:4:18
    51  .  .  .  .  .  .  .  .  .  .  Name: "string"
    52  .  .  .  .  .  .  .  .  .  }
    53  .  .  .  .  .  .  .  .  }
    54  .  .  .  .  .  .  .  .  Lbrace: singlefile.go:4:24
    55  .  .  .  .  .  .  .  .  Elts: []ast.Expr (len = 2) {
    56  .  .  .  .  .  .  .  .  .  0: *ast.KeyValueExpr {
    57  .  .  .  .  .  .  .  .  .  .  Key: *ast.BasicLit {
    58  .  .  .  .  .  .  .  .  .  .  .  ValuePos: singlefile.go:4:25
    59  .  .  .  .  .  .  .  .  .  .  .  Kind: STRING
    60  .  .  .  .  .  .  .  .  .  .  .  Value: "\"a\""
    61  .  .  .  .  .  .  .  .  .  .  }
    62  .  .  .  .  .  .  .  .  .  .  Colon: singlefile.go:4:28
    63  .  .  .  .  .  .  .  .  .  .  Value: *ast.BasicLit {
    64  .  .  .  .  .  .  .  .  .  .  .  ValuePos: singlefile.go:4:30
    65  .  .  .  .  .  .  .  .  .  .  .  Kind: STRING
    66  .  .  .  .  .  .  .  .  .  .  .  Value: "\"item a\""
    67  .  .  .  .  .  .  .  .  .  .  }
    68  .  .  .  .  .  .  .  .  .  }
    69  .  .  .  .  .  .  .  .  .  1: *ast.KeyValueExpr {
    70  .  .  .  .  .  .  .  .  .  .  Key: *ast.BasicLit {
    71  .  .  .  .  .  .  .  .  .  .  .  ValuePos: singlefile.go:4:40
    72  .  .  .  .  .  .  .  .  .  .  .  Kind: STRING
    73  .  .  .  .  .  .  .  .  .  .  .  Value: "\"b\""
    74  .  .  .  .  .  .  .  .  .  .  }
    75  .  .  .  .  .  .  .  .  .  .  Colon: singlefile.go:4:43
    76  .  .  .  .  .  .  .  .  .  .  Value: *ast.BasicLit {
    77  .  .  .  .  .  .  .  .  .  .  .  ValuePos: singlefile.go:4:45
    78  .  .  .  .  .  .  .  .  .  .  .  Kind: STRING
    79  .  .  .  .  .  .  .  .  .  .  .  Value: "\"item b\""
    80  .  .  .  .  .  .  .  .  .  .  }
    81  .  .  .  .  .  .  .  .  .  }
    82  .  .  .  .  .  .  .  .  }
    83  .  .  .  .  .  .  .  .  Rbrace: singlefile.go:4:53
    84  .  .  .  .  .  .  .  .  Incomplete: false
    85  .  .  .  .  .  .  .  }
    86  .  .  .  .  .  .  }
    87  .  .  .  .  .  }
    88  .  .  .  .  .  1: *ast.RangeStmt {
    89  .  .  .  .  .  .  For: singlefile.go:5:2
    90  .  .  .  .  .  .  Key: *ast.Ident {
    91  .  .  .  .  .  .  .  NamePos: singlefile.go:5:6
    92  .  .  .  .  .  .  .  Name: "k"
    93  .  .  .  .  .  .  .  Obj: *ast.Object {
    94  .  .  .  .  .  .  .  .  Kind: var
    95  .  .  .  .  .  .  .  .  Name: "k"
    96  .  .  .  .  .  .  .  .  Decl: *ast.AssignStmt {
    97  .  .  .  .  .  .  .  .  .  Lhs: []ast.Expr (len = 2) {
    98  .  .  .  .  .  .  .  .  .  .  0: *(obj @ 90)
    99  .  .  .  .  .  .  .  .  .  .  1: *ast.Ident {
   100  .  .  .  .  .  .  .  .  .  .  .  NamePos: singlefile.go:5:9
   101  .  .  .  .  .  .  .  .  .  .  .  Name: "v"
   102  .  .  .  .  .  .  .  .  .  .  .  Obj: *ast.Object {
   103  .  .  .  .  .  .  .  .  .  .  .  .  Kind: var
   104  .  .  .  .  .  .  .  .  .  .  .  .  Name: "v"
   105  .  .  .  .  .  .  .  .  .  .  .  .  Decl: *(obj @ 96)
   106  .  .  .  .  .  .  .  .  .  .  .  }
   107  .  .  .  .  .  .  .  .  .  .  }
   108  .  .  .  .  .  .  .  .  .  }
   109  .  .  .  .  .  .  .  .  .  TokPos: singlefile.go:5:11
   110  .  .  .  .  .  .  .  .  .  Tok: :=
   111  .  .  .  .  .  .  .  .  .  Rhs: []ast.Expr (len = 1) {
   112  .  .  .  .  .  .  .  .  .  .  0: *ast.UnaryExpr {
   113  .  .  .  .  .  .  .  .  .  .  .  OpPos: -
   114  .  .  .  .  .  .  .  .  .  .  .  Op: range
   115  .  .  .  .  .  .  .  .  .  .  .  X: *ast.Ident {
   116  .  .  .  .  .  .  .  .  .  .  .  .  NamePos: singlefile.go:5:20
   117  .  .  .  .  .  .  .  .  .  .  .  .  Name: "m"
   118  .  .  .  .  .  .  .  .  .  .  .  .  Obj: *(obj @ 32)
   119  .  .  .  .  .  .  .  .  .  .  .  }
   120  .  .  .  .  .  .  .  .  .  .  }
   121  .  .  .  .  .  .  .  .  .  }
   122  .  .  .  .  .  .  .  .  }
   123  .  .  .  .  .  .  .  }
   124  .  .  .  .  .  .  }
   125  .  .  .  .  .  .  Value: *(obj @ 99)
   126  .  .  .  .  .  .  TokPos: singlefile.go:5:11
   127  .  .  .  .  .  .  Tok: :=
   128  .  .  .  .  .  .  Range: singlefile.go:5:14
   129  .  .  .  .  .  .  X: *(obj @ 115)
   130  .  .  .  .  .  .  Body: *ast.BlockStmt {
   131  .  .  .  .  .  .  .  Lbrace: singlefile.go:5:22
   132  .  .  .  .  .  .  .  List: []ast.Stmt (len = 1) {
   133  .  .  .  .  .  .  .  .  0: *ast.AssignStmt {
   134  .  .  .  .  .  .  .  .  .  Lhs: []ast.Expr (len = 1) {
   135  .  .  .  .  .  .  .  .  .  .  0: *ast.IndexExpr {
   136  .  .  .  .  .  .  .  .  .  .  .  X: *ast.Ident {
   137  .  .  .  .  .  .  .  .  .  .  .  .  NamePos: singlefile.go:6:3
   138  .  .  .  .  .  .  .  .  .  .  .  .  Name: "m"
   139  .  .  .  .  .  .  .  .  .  .  .  .  Obj: *(obj @ 32)
   140  .  .  .  .  .  .  .  .  .  .  .  }
   141  .  .  .  .  .  .  .  .  .  .  .  Lbrack: singlefile.go:6:4
   142  .  .  .  .  .  .  .  .  .  .  .  Index: *ast.Ident {
   143  .  .  .  .  .  .  .  .  .  .  .  .  NamePos: singlefile.go:6:5
   144  .  .  .  .  .  .  .  .  .  .  .  .  Name: "k"
   145  .  .  .  .  .  .  .  .  .  .  .  .  Obj: *(obj @ 93)
   146  .  .  .  .  .  .  .  .  .  .  .  }
   147  .  .  .  .  .  .  .  .  .  .  .  Rbrack: singlefile.go:6:6
   148  .  .  .  .  .  .  .  .  .  .  }
   149  .  .  .  .  .  .  .  .  .  }
   150  .  .  .  .  .  .  .  .  .  TokPos: singlefile.go:6:8
   151  .  .  .  .  .  .  .  .  .  Tok: =
   152  .  .  .  .  .  .  .  .  .  Rhs: []ast.Expr (len = 1) {
   153  .  .  .  .  .  .  .  .  .  .  0: *ast.BinaryExpr {
   154  .  .  .  .  .  .  .  .  .  .  .  X: *ast.BasicLit {
   155  .  .  .  .  .  .  .  .  .  .  .  .  ValuePos: singlefile.go:6:10
   156  .  .  .  .  .  .  .  .  .  .  .  .  Kind: STRING
   157  .  .  .  .  .  .  .  .  .  .  .  .  Value: "\"reassigned: \""
   158  .  .  .  .  .  .  .  .  .  .  .  }
   159  .  .  .  .  .  .  .  .  .  .  .  OpPos: singlefile.go:6:25
   160  .  .  .  .  .  .  .  .  .  .  .  Op: +
   161  .  .  .  .  .  .  .  .  .  .  .  Y: *ast.Ident {
   162  .  .  .  .  .  .  .  .  .  .  .  .  NamePos: singlefile.go:6:27
   163  .  .  .  .  .  .  .  .  .  .  .  .  Name: "v"
   164  .  .  .  .  .  .  .  .  .  .  .  .  Obj: *(obj @ 102)
   165  .  .  .  .  .  .  .  .  .  .  .  }
   166  .  .  .  .  .  .  .  .  .  .  }
   167  .  .  .  .  .  .  .  .  .  }
   168  .  .  .  .  .  .  .  .  }
   169  .  .  .  .  .  .  .  }
   170  .  .  .  .  .  .  .  Rbrace: singlefile.go:7:2
   171  .  .  .  .  .  .  }
   172  .  .  .  .  .  }
   173  .  .  .  .  }
   174  .  .  .  .  Rbrace: singlefile.go:8:1
   175  .  .  .  }
   176  .  .  }
   177  .  }
   178  .  FileStart: singlefile.go:1:1
   179  .  FileEnd: singlefile.go:8:3
   180  .  Scope: *ast.Scope {
   181  .  .  Objects: map[string]*ast.Object (len = 1) {
   182  .  .  .  "main": *(obj @ 11)
   183  .  .  }
   184  .  }
   185  .  Unresolved: []*ast.Ident (len = 2) {
   186  .  .  0: *(obj @ 45)
   187  .  .  1: *(obj @ 49)
   188  .  }
   189  .  GoVersion: ""
   190  }

このASTを眺めると

  • *ast.RangeStmt が range 文ぽい
  • *ast.RangeStmt.X に range access 対象がはいってそう
  • *ast.RangeStmt.Body.List にloop内の文がありそう
  • ループ内で *ast.AssignStmt になっているのが代入そう
  • *ast.AssignStmt.Lhs が左辺ぽいので、そこからよしなに値を眺めて range access 対象と比較すればよさそう
  • 同一objectだったら怒れば勝ちそう!

みたいなことがわかります。

実装してみる

このあたりのドキュメントをみると値の比較には go/types.Object が利用できそうです。

example/gotypes at master · golang/example · GitHub

ふつうに == で比較するだけでいいらしい。便利〜

実行ターゲットになってるパッケージのtype infoについては analysis.Pass に突っ込まれており、識別子から go/types/Object を引っ張り出す ObjectOf() が生えてるので *ast.Ident さえあればよしなに取り出せます https://cs.opensource.google/go/x/tools/+/refs/tags/v0.14.0:go/analysis/analysis.go;l=99

ポインタとかの場合も考慮してよしなに書くと以下のかんじです。

func getObject(pass *analysis.Pass, x ast.Expr) types.Object {
    switch x.(type) {
    case *ast.Ident:
        return pass.TypesInfo.ObjectOf(x.(*ast.Ident))
    case *ast.StarExpr:
        return getObject(pass, x.(*ast.StarExpr).X)
    case *ast.ParenExpr:
        return getObject(pass, x.(*ast.ParenExpr).X)
  }
    return nil
}

実行ターゲットに含まれてないパッケージでExportedなmapをもってる可能性もあるのでそのあたりもよしなに考慮したいので以下も追加します。 *ast.SelectorExprfmt.Println() みたいなimportしたパッケージへのアクセスなどに使われます。あとメソッドへのアクセスにも使われます。

analysis.Pass にはまたまた types.Package とかいう便利なやつが突っ込まれており、importしてるpackageなどが取れます。 https://cs.opensource.google/go/x/tools/+/refs/tags/v0.14.0:go/analysis/analysis.go;l=98

パッケージの名前 と *ast.SelectorExpr.X.Name ( fmt.Println() でいう fmt) の値が一致してたら、該当のパッケージのscopeとして types.Object をさがしにいく感じにすればよいです。

func getObject(pass *analysis.Pass, x ast.Expr) types.Object {
  ...
  case *ast.SelectorExpr:
        sel := x.(*ast.SelectorExpr)
        id, ok := sel.X.(*ast.Ident)
        if !ok {
            return nil
        }
        pkg := id.Name
        call := sel.Sel.Name
        imports := pass.Pkg.Imports()
        for i := range imports {
            if pkg == imports[i].Name() {
                return imports[i].Scope().Lookup(call)
            }
        }
    }
    return nil
}

ちなここでつかってる Lookup() は賢くて、中で既にloadずみのやつをなんども読み取らないように sync.Once をちゃんと使ってくれているので気にせずしばいてOK

https://cs.opensource.google/go/go/+/refs/tags/go1.21.3:src/go/types/scope.go;l=251-274

あと、今回はmapだけ判断したいので以下みたいな感じでとってきたObjectをmapにできると良さげな感じがします。

rangeTarget := getObject(pass, rstmt.X)
if rangeTarget == nil {
    return
}
_, ok = rangeTarget.Type().Underlying().(*types.Map)
if !ok {
    return
}

ここで rangeTarget.Type() が pointer のことも考慮すると、型をderefする処理もあると良さそうなので書く

func run(pass *analysis.Pass) (interface{}, error) {
  ...
  inspect.Preorder(nodeFilter, func(n ast.Node) {
    ...
    _, ok = deref(rangeTarget.Type()).Underlying().(*types.Map)
        if !ok {
          return
        }
    ...
  }
  return nil, nil
}
...
func deref(typ types.Type) types.Type {
    if ptr, ok := typ.Underlying().(*types.Pointer); ok {
        return deref(ptr.Elem())
    }
    return typ
}

これでよしなに動くやつになった!

全体像は以下

package mapbreak

import (
    "go/ast"
    "go/types"

    "golang.org/x/tools/go/analysis"
    "golang.org/x/tools/go/analysis/passes/inspect"
    "golang.org/x/tools/go/ast/inspector"
)

var Analyzer = &analysis.Analyzer{
    Name: "mapbreak",
    Doc:  Doc,
    Run:  run,
    Requires: []*analysis.Analyzer{
        inspect.Analyzer,
    },
}

const Doc = "mapbreak detects if there is map reassignment in the range access"

func run(pass *analysis.Pass) (interface{}, error) {
    inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)

    nodeFilter := []ast.Node{
        (*ast.RangeStmt)(nil),
    }

    inspect.Preorder(nodeFilter, func(n ast.Node) {
        rstmt, ok := n.(*ast.RangeStmt)
        if !ok {
            return
        }

        rangeTarget := getObject(pass, rstmt.X)
        if rangeTarget == nil {
            return
        }
        _, ok = deref(rangeTarget.Type()).Underlying().(*types.Map)
        if !ok {
            return
        }

        for _, stmt := range rstmt.Body.List {
            assign, ok := stmt.(*ast.AssignStmt)
            if !ok {
                continue
            }
            idx, ok := assign.Lhs[0].(*ast.IndexExpr)
            if !ok {
                continue
            }
            assignee := getObject(pass, idx.X)
            if assignee == rangeTarget {
                pass.Reportf(stmt.Pos(), "detected range access to map and reassigning it")
            }
        }
    })

    return nil, nil
}

func deref(typ types.Type) types.Type {
    if ptr, ok := typ.Underlying().(*types.Pointer); ok {
        return deref(ptr.Elem())
    }
    return typ
}

func getObject(pass *analysis.Pass, x ast.Expr) types.Object {
    switch x.(type) {
    case *ast.Ident:
        return pass.TypesInfo.ObjectOf(x.(*ast.Ident))
    case *ast.StarExpr:
        return getObject(pass, x.(*ast.StarExpr).X)
    case *ast.ParenExpr:
        return getObject(pass, x.(*ast.ParenExpr).X)
    case *ast.SelectorExpr:
        sel := x.(*ast.SelectorExpr)
        id, ok := sel.X.(*ast.Ident)
        if !ok {
            return nil
        }
        pkg := id.Name
        call := sel.Sel.Name
        imports := pass.Pkg.Imports()
        for i := range imports {
            if pkg == imports[i].Name() {
                return imports[i].Scope().Lookup(call)
            }
        }
    }
    return nil
}

さいごに

Go の静的解析、エコシステムが成熟してるのは知ってたけど試したことはなかったのでちょうどよく作業できて良かった!

外部パッケージを含むケースでもよしなにできたりするのはめっちゃ便利で、よくできてるなと思いました。

今回みたいな型を知りたいケースは機械的な文字列一致でやるのは難しいので、高度な判定ができるのはかなりありがたかったです。

また、 https://github.com/golang/example/tree/master/gotypes をみて表現力かなりあることがわかったのでまた遊んでみたいですね!

LayerX アドベントカレンダー (概念) は今日から始まるらしいので引き続き他の記事も楽しみにしててください〜概念概念