OPA・Regoでコーディング規約が守られている事を検査する

## 概要

チームやプロジェクトで独自に定めたコーディング規約が遵守されていることを確認するために、OPA・Regoを使ってコードを検査する。Golangでの実装を想定している。

## 仕組み

まず、検査対象のコードをASTに変換する。ASTは、抽象構文木のことで、コードの構造を表現したデータ構造のこと。ASTを使うことで、コードの構造をプログラムで扱えるようになる。
Regoで記述したコーディング規約をポリシーとして、ASTをインプットとしてOPAに渡すことで、コーディング規約に違反している箇所を検出することができる。

## goast

上記を簡単に実現するために、[goast](https://github.com/m-mizutani/goast)というツールを使用する。goastはユーザが定義したポリシーをもとに上記をまとめて行ってくれる。出力した結果に`pos`という名前でPos値を渡すことで、出力に違反しているコードを表示することもできる。

```rego
res := {
    "msg": "fmt.Println is not allowed",
    "pos": input.Node.X.Fun.X.NamePos,
    "sev": "ERROR"
}
```

## 実装
goastを使って以下のようなルールを記述してみる。

### 1. メソッドのレシーバは1文字とする
- 検査対象のGoコード(main.go)
```go
package main

func main() {}

type Test struct{}

func (t Test) Valid() {}

func (tt Test) Invalid() {}
```

- ポリシー(policy.rego)
```rego
# メソッドのレシーバ名は1文字とする
fail[res] {
    input.Kind == "FuncDecl"
    input.Node.Recv != null
    count(input.Node.Recv.List[x].Names[y].Name) != 1

    res := {
        "msg": "method receiver name should be 1 character",
        "pos": input.Node.Recv.List[x].Names[y].NamePos,
        "sev": "ERROR"
    }
}
```

- 実行結果
```shell
$ goast eval -p ./policy.rego main.go
[main.go:9] - method receiver name should be 1 character

func (tt Test) Invalid() {}
      ~~~~~~~~~~~~~~~~~~~~~


        Detected 1 violations
```

- テスト(policy_test.rego)をテーブル駆動テスト風に記述してみる

```rego
package goast

test_policy {
    not _test_policy
}

_test_policy {
    testCases := [
        {
            "name": "レシーバがない",
            "wantErr": false,
            "data": {
                  "Path": "main.go",
                  "FileName": "main.go",
                  "DirName": ".",
                  "Node": {
                    "Doc": null,
                    "Recv": null,
                    "Name": {
                      "NamePos": 20,
                      "Name": "main",
                      "Obj": null
                    },
                    "Type": {
                      "Func": 15,
                      "TypeParams": null,
                      "Params": {
                        "Opening": 24,
                        "List": [],
                        "Closing": 25
                      },
                      "Results": null
                    },
                    "Body": {
                      "Lbrace": 27,
                      "List": [],
                      "Rbrace": 28
                    }
                  },
                  "Kind": "FuncDecl"
            }
        },
        {
            "name": "レシーバが1文字",
            "wantErr": false,
            "data": {
                  "Path": "main.go",
                  "FileName": "main.go",
                  "DirName": ".",
                  "Node": {
                    "Doc": null,
                    "Recv": {
                      "Opening": 56,
                      "List": [
                        {
                          "Doc": null,
                          "Names": [
                            {
                              "NamePos": 57,
                              "Name": "t",
                              "Obj": null
                            }
                          ],
                          "Type": {
                            "NamePos": 59,
                            "Name": "Test",
                            "Obj": null
                          },
                          "Tag": null,
                          "Comment": null
                        }
                      ],
                      "Closing": 63
                    },
                    "Name": {
                      "NamePos": 65,
                      "Name": "Valid",
                      "Obj": null
                    },
                    "Type": {
                      "Func": 51,
                      "TypeParams": null,
                      "Params": {
                        "Opening": 70,
                        "List": [],
                        "Closing": 71
                      },
                      "Results": null
                    },
                    "Body": {
                      "Lbrace": 73,
                      "List": [],
                      "Rbrace": 74
                    }
                  },
                  "Kind": "FuncDecl"
            }
        },
        {
            "name": "レシーバが2文字",
            "wantErr": true,
            "data": {
                  "Path": "main.go",
                  "FileName": "main.go",
                  "DirName": ".",
                  "Node": {
                    "Doc": null,
                    "Recv": {
                      "Opening": 106,
                      "List": [
                        {
                          "Doc": null,
                          "Names": [
                            {
                              "NamePos": 107,
                              "Name": "tt",
                              "Obj": null
                            }
                          ],
                          "Type": {
                            "NamePos": 110,
                            "Name": "Test",
                            "Obj": null
                          },
                          "Tag": null,
                          "Comment": null
                        }
                      ],
                      "Closing": 114
                    },
                    "Name": {
                      "NamePos": 116,
                      "Name": "Invalid",
                      "Obj": null
                    },
                    "Type": {
                      "Func": 101,
                      "TypeParams": null,
                      "Params": {
                        "Opening": 123,
                        "List": [],
                        "Closing": 124
                      },
                      "Results": null
                    },
                    "Body": {
                      "Lbrace": 126,
                      "List": [],
                      "Rbrace": 127
                    }
                  },
                  "Kind": "FuncDecl"
            }
        }
    ]

    tc := testCases[_]
    out := fail with input as tc.data
    tc.wantErr != (count(out) > 0)
}
```

実行
```shell
$ opa test -v .
policy_test.rego:
data.goast.test_policy: PASS (687.958µs)
--------------------------------------------------------------------------------
PASS: 1/1
```

### 2. Publicな関数にはコメントをつける
- Goコード
```go
package main

func main() {}

// Valid is valid
func Valid()   {}
func valid()   {}
func Invalid() {}
```

- ポリシー
```rego
# Publicな関数にはコメントをつける
fail[res] {
    input.Kind == "FuncDecl"
    is_uppercase(substring(input.Node.Name.Name, 0, 1))
    input.Node.Doc == null

    res := {
        "msg": "public function should have comment",
        "pos": input.Node.Name.NamePos,
        "sev": "ERROR"
    }
}

is_uppercase(str) {
    str == upper(str)
}
```

- 実行
```shell
goast eval -p ./policy.rego main.go
[main.go:8] - public function should have comment

func Invalid() {}
     ~~~~~~~~~~~~


        Detected 1 violations
```

### 3. handler pkg以外がhttp pkgに依存することを禁止する
- ディレクトリ構成
```shell
internal
├── handler
│   └── handler.go
└── service
    └── service.go

3 directories, 2 files
```

- handler.go
```go
package handler

import "net/http"

func handler() {
	_, _ = http.Get("http://example.com/")
}
```

- service.go
```go
package service

import "net/http"

func service() {
	_, _ = http.Get("http://example.com/")
}
```

- ポリシー
```rego
# handler pkg以外がhttp pkgに依存することを禁止する
fail[res] {
    input.Kind == "File"
    input.DirName != "internal/handler"
    input.Node.Imports[x].Path.Value == "\"net/http\""

    res := {
        "msg": "handler pkg should not depend on http pkg",
        "pos": input.Node.Imports[x].Path.ValuePos,
        "sev": "ERROR"
    }
}
```

- 実行結果
```shell
$ goast eval -p ./policy.rego internal
[internal/service/service.go:3] - handler pkg should not depend on http pkg

import "net/http"
       ~~~~~~~~~~


        Detected 1 violations
```

## 参考

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