Golangのfor rangeでのポインタ問題をLinterで防ぐ

# for range内でのポインタの挙動

まず以下のコードを実行すると、結果はどうなるでしょうか。

```go:main.go
package main

import "fmt"

type User struct {
	id   int
	name string
}

func main() {
	users := []User{{1, "John"}, {2, "Melissa"}, {3, "Robert"}}
	var names []*string
	for _, v := range users {
		names = append(names, &v.name)
	}
	for _, v := range names {
		fmt.Println(*v)
	}
}
```

結果はこうなります。すべて最後のUserの値で置き換えられてしまっています。

```
Robert
Robert
Robert
```

これは`for _, v := range`文の中では、毎回同じアドレスが使われていることに起因します。

これを防ぐためには以下のように`[i]`でアクセスすることで対応できます。

```go:main_fix.go
package main

import "fmt"

type User struct {
	id   int
	name string
}

func main() {
	users := []User{{1, "John"}, {2, "Melissa"}, {3, "Robert"}}
	var names []*string
	for i := range users {
		names = append(names, &(users[i]).name)
	}
	for i := range names {
		fmt.Println(*names[i])
	}
}
```

出力結果

```
John
Melissa
Robert
```

今回は`main.go`のような書き方になってしまっているファイルをLinterで発見する仕組みを考えます。

# 選定

GolangでLinterといえば、[golangci-lint](https://github.com/golangci/golangci-lint)が思いつくと思います。

カスタムルールを作成する方法も紹介されていますが、`.so`を書き出す必要があったりして、既存のプロジェクトに組み込むのは拡張性などの面で少しハードルがあります。

https://golangci-lint.run/contributing/new-linters/#how-to-add-a-private-linter-to-golangci-lint

保守性と拡張性を考えた時、goastというものが使えそうです

https://github.com/m-mizutani/goast

# goast

以下の開発者の記事によると、

> Goのコードを読み込み、コードの抽象表現であるAST（Abstract Syntax Tree、構文抽象木）をRegoで記述されたポリシーによって評価します。ASTに関する説明はこちらの資料などがわかりやすいかと思います。 parser パッケージを使ってGoソースコードのASTを取得し、これをRegoのポリシーで評価します。評価はファイル全体のASTを一度だけ渡す、あるいはASTのノード毎に評価するモードを用意しています。

とのことです。

https://zenn.dev/mizutani/articles/go-static-analysis-with-rego

そのため、Regoでカスタムルールを書いていくことになります。


# 先に結論


プロジェクトディレクトリに`.goast`ディレクトリを作っておきます。ここの中に.regoファイルを書いておきます。

これでfor rangeのvalueのアドレスを、append関数の中で利用しようとした場合にエラーを吐き出します。

```rego:do_not_append_for_range_value_memory_address.rego
package goast

fail[res] {
    input.Kind == "RangeStmt"
    input.Node.Value != null
    [appendPath, appendValue] := walk(input.Node.Body.List)
    appendValue[_].Fun.Name == "append" # search for append func only
    [path, value] := walk(appendValue)
    value.Op == 17 # operator: &
    [pathDetail, valueDetail] := walk(value)
    valueDetail.Name == input.Node.Value.Name

    res := {
        "msg": "do not append for range value memory address",
        "pos": value.OpPos,
        "sev": "ERROR",
    }
}
```

次にgoastコマンドを実行するため、インストールしておきます

```sh
go install github.com/m-mizutani/goast/cmd/goast@latest
```

localからコマンドラインでLinterを実行する場合は以下のコマンドを実行します。

```sh
goast eval -p .goast --ignore-auto-generated **/*.go
```

試しに冒頭の`main.go`に対して、コマンド実行すると、以下の出力が得られます。

```
$ goast eval -p .goast/do_not_append_for_range_value_memory_address.rego main.go
[main.go:14] - do not append for range value memory address: "&v"

		names = append(names, &v.name)
		                      ~~~~~~~~


	Detected 1 violations

```

Github Actionsを利用したい場合は、[goast-action](https://github.com/m-mizutani/goast-action)を用意していただいているため簡単に設定できます。


```yml:ci.yml
name: goast

on:
  pull_request:

jobs:
  eval:
    runs-on: ubuntu-latest
    steps:
      - name: checkout
        uses: actions/checkout@v2
      - name: run goast Lint
        uses: m-mizutani/goast-action@main
        with:
          policy: ./.goast 
          format: text 
          source: ./
          ignore_auto_generated: true
```

# Regoファイルの作成

まず、goastのRegoファイルにいくつかルールがあるようです。

> - package が goast でなければならない
> - 入力： input には Path、Kind のようなメタ情報と、実際のASTである Node が渡される
> - 出力：違反があった場合、 fail という変数に以下のフィールドをもつ構造体を入れる
> - msg: 違反内容のメッセージ（文字列）
> - pos: ファイル内の位置を示す整数値
> - sev: 深刻度。INFO, WARNING, もしくは ERROR

そのため基本フォーマットは以下のようになります

```rego
package goast

fail[res] {
    # ここにルールを記述

    res := {
        "msg": "エラーメッセージ",
        "pos": エラーの位置.~~Pos,
        "sev": "ERROR",
    }
}
```

次にルールの書きかたですが、まずはASTを出力して確認したほうが良いです。


```sh
goast dump main.go >> ast.json
```

出力された`ast.json`から、該当のfor文にあたるものを探します

```json:ast.json(該当箇所以外省略)
{
  "Path": "main.go",
  "FileName": "main.go",
  "DirName": ".",
  "Node": {
    "For": 171,
    "Key": {
      "NamePos": 175,
      "Name": "_",
      "Obj": null
    },
    "Value": {
      "NamePos": 178,
      "Name": "v",
      "Obj": null
    },
    "TokPos": 180,
    "Tok": 47,
    "Range": 183,
    "X": {
      "NamePos": 189,
      "Name": "users",
      "Obj": null
    },
    "Body": {
      "Lbrace": 195,
      "List": [
        {
          "Lhs": [
            {
              "NamePos": 199,
              "Name": "names",
              "Obj": null
            }
          ],
          "TokPos": 205,
          "Tok": 42,
          "Rhs": [
            {
              "Fun": {
                "NamePos": 207,
                "Name": "append",
                "Obj": null
              },
              "Lparen": 213,
              "Args": [
                {
                  "NamePos": 214,
                  "Name": "names",
                  "Obj": null
                },
                {
                  "OpPos": 221,
                  "Op": 17,
                  "X": {
                    "X": {
                      "NamePos": 222,
                      "Name": "v",
                      "Obj": null
                    },
                    "Sel": {
                      "NamePos": 224,
                      "Name": "name",
                      "Obj": null
                    }
                  }
                }
              ],
              "Ellipsis": 0,
              "Rparen": 228
            }
          ]
        }
      ],
      "Rbrace": 231
    }
  },
  "Kind": "RangeStmt"
}
```

`"Op": 17`が、演算子(Op)「&」(17)を表しています。

次にRegoファイルの書き方です。

基本的にはASTのjsonを探索していって、すべての条件がマッチしたものだけが最後の`res`にたどり着いてエラー文を吐きます。

たとえば以下のルールでは、for rangeのvalueの使用自体を禁止できます。


```rego:do_not_use_for_range_value.rego
package goast

fail[res] {
    input.Kind == "RangeStmt"
    input.Node.Value != null

    res := {
        "msg": "do not use for range value",
        "pos": input.Node.Value.NamePos,
        "sev": "ERROR",
    }
}
```

```
$ goast eval -p .goast/do_not_use_for_range_value.rego main.go
[main.go:13] - do not use for range value

	for _, v := range users {
	       ~~~~~~~~~~~~~~~~~~

[main.go:16] - do not use for range value

	for _, v := range names {
	       ~~~~~~~~~~~~~~~~~~


	Detected 2 violations

```

複雑な探索をしたいときは、[walk関数](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-graph-walk)が便利です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437488/85e67907-acb5-1df8-2427-9d703dc2cad6.png)
`path`はあるkeyまでのkeyの配列、`value`はそのkey以下のjsonです。

例えば、以下のように書くと

```rego

fail[res] {
    input.Kind == "RangeStmt"
    input.Node.Value != null
    [path, value] := walk(input)
    value.Op == 17
    
}
```

pathとvalueの値はそれぞれ

```
path:  ["Node","Body","List",0,"Rhs",0,"Args",1]
value: {"Op":17,"OpPos":221,"X":{"Sel":{"Name":"name","NamePos":224,"Obj":null},"X":{"Name":"v","NamePos":222,"Obj":null}}}
```

となります。


:::note info
ちなみにdebug時にpathやvalueを確認したい場合は、[json.marshal関数](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-encoding-jsonmarshal)で文字列に変換すればresのmsgに含ませたりすることで書き出せます。
:::

これを利用すれば、for rangeの中で&vの使用を禁止するルールがかけます。

```rego:do_not_use_for_range_value_memory_address.rego
package goast

fail[res] {
    input.Kind == "RangeStmt"
    input.Node.Value != null
    [path, value] := walk(input)
    value.Op == 17 # operator: &
    [pathDetail, valueDetail] := walk(value)
    valueDetail.Name == input.Node.Value.Name

    res := {
        "msg": "do not use for range value memory address",
        "pos": value.OpPos,
        "sev": "ERROR",
    }
}
```

append関数の中での&vの使用を禁止したければ、appendがあるリストと同階層から探索すればよさそうなので、以下のようになります。


```rego:do_not_append_for_range_value_memory_address.rego
package goast

fail[res] {
    input.Kind == "RangeStmt"
    input.Node.Value != null
    [appendPath, appendValue] := walk(input)
    appendValue[_].Fun.Name == "append" # search for append func only
    [path, value] := walk(appendValue)
    value.Op == 17 # operator: &
    [pathDetail, valueDetail] := walk(value)
    valueDetail.Name == input.Node.Value.Name

    res := {
        "msg": "do not append for range value memory address",
        "pos": value.OpPos,
        "sev": "ERROR",
    }
}
```