Protocol Buffer - protolint の説明と CI 導入手順

# 追記

VSCode と Intellij IDEA をはじめとした Jetbrains IDE 向けのプラグインが用意されている

- [plexsystems/vscode-protolint: A protobuf linter for visual studio code](https://github.com/plexsystems/vscode-protolint)
- [yoheimuta/intellij-protolint: A protobuf linter for JetBrains IDEs](https://github.com/yoheimuta/intellij-protolint)

# protolint

protolint は Protocol Buffer ファイル(proto3) のリントを行うコマンドラインツールである。

- [yoheimuta/protolint: A tool to enforce Protocol Buffer style and conventions.](https://github.com/yoheimuta/protolint)

現在のところ、Google のスタイルガイド準拠をサポートしている。

- [Style Guide  |  Protocol Buffers  |  Google Developers](https://developers.google.com/protocol-buffers/docs/style)

特徴は次の通り。

- 処理が早い。内部的にはパースするだけなので、少なくともコンパイラーのアドオンによるものより高速。
- Google のスタイルガイドとルールが 1対1 で対応している。
- ファイルのコメントでリントルールの除外ができる。

ファイルのコメントでリントルールの除外ができるのは、すでにリリース済みのプロダクトでは特に有用。
除外範囲を最小限にしつつ、新規の定義にはルールを適用できる。

```proto
enum Foo {
  // protolint:disable:next ENUM_FIELD_NAMES_UPPER_SNAKE_CASE
  firstValue = 0;    // no error
  second_value = 1;  // protolint:disable:this ENUM_FIELD_NAMES_UPPER_SNAKE_CASE
  THIRD_VALUE = 2;   // spits out an error
}
```

## ルール

現在（2018.12.27）、サポートしているルールは次の 6 つ。

| ID                                | Purpose                                                                  |
|-----------------------------------|--------------------------------------------------------------------------|
| ENUM_FIELD_NAMES_UPPER_SNAKE_CASE | enum フィールド名が CAPITALS_WITH_UNDERSCORES かチェックする        |
| ENUM_NAMES_UPPER_CAMEL_CASE       | enum 名 が CamelCase (with an initial capital) かチェックする    |
| FIELD_NAMES_LOWER_SNAKE_CASE      | field 名が underscore_separated_names かチェックする            |
| MESSAGE_NAMES_UPPER_CAMEL_CASE    | message 名が CamelCase (with an initial capital) かチェックする |
| RPC_NAMES_UPPER_CAMEL_CASE        | rpc 名が CamelCase (with an initial capital) かチェックする     |
| SERVICE_NAMES_UPPER_CAMEL_CASE    | service 名が CamelCase (with an initial capital) かチェックする |

以下はそれぞれのルールの例。
`-` がスタイル違反で、`+` がスタイル準拠。

__ENUM_FIELD_NAMES_UPPER_SNAKE_CASE__

```diff
enum Foo {
-  firstValue = 0;
+  FIRST_VALUE = 0;
-  second_value = 1;
+  SECOND_VALUE = 1;
}
```

__ENUM_NAMES_UPPER_CAMEL_CASE__

```diff
- enum foobar {
+ enum FooBar {
  FIRST_VALUE = 0;
  SECOND_VALUE = 1;
}
```

__FIELD_NAMES_LOWER_SNAKE_CASE__

```diff
message SongServerRequest {
-  required string SongName = 1;
+  required string song_name = 1;
}
```

__MESSAGE_NAMES_UPPER_CAMEL_CASE__

```diff
- message song_server_request {
+ message SongServerRequest {
  required string SongName = 1;
  required string song_name = 1;
}
```

__RPC_NAMES_UPPER_CAMEL_CASE__

```diff
service FooService {
-  rpc get_something(FooRequest) returns (FooResponse);
+  rpc GetSomething(FooRequest) returns (FooResponse);
}
```

__RPC_NAMES_UPPER_CAMEL_CASE__

```diff
- service foo_service {
+ service FooService {
  rpc get_something(FooRequest) returns (FooResponse);
  rpc GetSomething(FooRequest) returns (FooResponse);
}
```

# 使い方

```
pl lint example.proto example2.proto # file mode, specify multiple specific files
pl lint .                            # directory mode, search for all .proto files recursively
pl .                                 # same as "pl lint ."
pl list                              # list all current lint rules being used
```

例えば、スタイル違反が見つかると、以下のようなエラーメッセージが出る。また、exit code 1 を返す。

```
❯❯❯ pl .
[v1/master/master.proto:30:5] Field name "shouldInvalidate" must be LowerSnakeCase
[v1/search/search.proto:45:5] Field name "brandEnds" must be LowerSnakeCase
[v1/support/marketing.proto:62:5] Field name "announcementInApp" must be LowerSnakeCase
[v1/support/marketing.proto:64:5] Field name "announcementApns" must be LowerSnakeCase
[v1/values/itemContentCondition.proto:35:5] EnumField name "Unidentified" must be UpperSnakeCase

❯❯❯ ~/.go/bin/pl list
ENUM_FIELD_NAMES_UPPER_SNAKE_CASE: Verifies that all enum field names are CAPITALS_WITH_UNDERSCORES.
ENUM_NAMES_UPPER_CAMEL_CASE: Verifies that all enum names are CamelCase (with an initial capital).
FIELD_NAMES_LOWER_SNAKE_CASE: Verifies that all field names are underscore_separated_names.
MESSAGE_NAMES_UPPER_CAMEL_CASE: Verifies that all message names are CamelCase (with an initial capital).
RPC_NAMES_UPPER_CAMEL_CASE: Verifies that all rpc names are CamelCase (with an initial capital).
SERVICE_NAMES_UPPER_CAMEL_CASE: Verifies that all service names are CamelCase (with an initial capital).
```

# CircleCI での導入手順

.circleci/config.yaml を作成するだけ。
以下はリポジトリ直下すべてに再帰的に lint 処理を行う。

```yaml
version: 2
jobs:
  lint:
    docker:
      - image: circleci/golang:1.11.4
        environment:
          GO111MODULE: "on"
    steps:
      - checkout
      - run:
          name: Enforce Protocol Buffer style and conventions.
          command: |
            go get -u -v github.com/yoheimuta/protolint/cmd/pl
            pl lint .
workflows:
  version: 2
  lint:
    jobs:
      - lint
```

# まとめ

十分気をつけていると思っていたプロジェクトでも、スタイル違反が見つかったので、早速導入した。
最初からリントプロセスは CI に入れておくと良い。リリースしてしまうと、修正するのは大変なため。