0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

概要

チームやプロジェクトで独自に定めたコーディング規約が遵守されていることを確認するために、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

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?