概要
チームやプロジェクトで独自に定めたコーディング規約が遵守されていることを確認するために、OPA・Regoを使ってコードを検査する。Golangでの実装を想定している。
仕組み
まず、検査対象のコードをASTに変換する。ASTは、抽象構文木のことで、コードの構造を表現したデータ構造のこと。ASTを使うことで、コードの構造をプログラムで扱えるようになる。
Regoで記述したコーディング規約をポリシーとして、ASTをインプットとしてOPAに渡すことで、コーディング規約に違反している箇所を検出することができる。
goast
上記を簡単に実現するために、goastというツールを使用する。goastはユーザが定義したポリシーをもとに上記をまとめて行ってくれる。出力した結果にpos
という名前でPos値を渡すことで、出力に違反しているコードを表示することもできる。
res := {
"msg": "fmt.Println is not allowed",
"pos": input.Node.X.Fun.X.NamePos,
"sev": "ERROR"
}
実装
goastを使って以下のようなルールを記述してみる。
1. メソッドのレシーバは1文字とする
- 検査対象のGoコード(main.go)
package main
func main() {}
type Test struct{}
func (t Test) Valid() {}
func (tt Test) Invalid() {}
- ポリシー(policy.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"
}
}
- 実行結果
$ 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)をテーブル駆動テスト風に記述してみる
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)
}
実行
$ opa test -v .
policy_test.rego:
data.goast.test_policy: PASS (687.958µs)
--------------------------------------------------------------------------------
PASS: 1/1
2. Publicな関数にはコメントをつける
- Goコード
package main
func main() {}
// Valid is valid
func Valid() {}
func valid() {}
func Invalid() {}
- ポリシー
# 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)
}
- 実行
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に依存することを禁止する
- ディレクトリ構成
internal
├── handler
│ └── handler.go
└── service
└── service.go
3 directories, 2 files
- handler.go
package handler
import "net/http"
func handler() {
_, _ = http.Get("http://example.com/")
}
- service.go
package service
import "net/http"
func service() {
_, _ = http.Get("http://example.com/")
}
- ポリシー
# 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"
}
}
- 実行結果
$ 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