ReviewdogのGitHub ActionsでGoのlintをかけてPRに表示する

githubgolang

Reviewdogはlinterの結果をPRにコメントしてくれるツール。 差分のみに適用することができるので段階的に改善していくこともできる。

Staticcheck

StaticcheckはdeprecatedになったGolintのリポジトリで移行先として紹介されているlinter。

$ go install honnef.co/go/tools/cmd/staticcheck@latest
$ staticcheck --version
staticcheck 2021.1 (v0.2.0)

典型的なミスや非効率な書き方など様々な項目がチェックされる。

package main

import (
  "context"
  "encoding/json"
  "fmt"
  "logging"
  "strings"
)

type Color int

const (
  Red  Color = 1 // main.go:14:2: only the first constant in this group has an explicit type (SA9004)
  Blue       = 2
)

func f(ctx context.Context, str []byte) error {
  var m map[string]string
  m["A"] = "default"                             // main.go:13:2: assignment to nil map (SA5000)
  if err := json.Unmarshal(str, m); err != nil { // main.go:14:32: json.Unmarshal expects to unmarshal into a pointer, but the provided value is not a pointer (SA1014)
    return err
  }
  l := *&m // main.go:17:7: *&x will be simplified to x. It will not copy x. (SA4001)
  l["A"] = ""
  if strings.ToLower(m["A"]) == strings.ToLower(m["X"]) { // main.go:19:5: should use strings.EqualFold instead (SA6005)
    fmt.Println(fmt.Sprintf("%v -> %s", m, l)) // main.go:27:3: should use fmt.Printf instead of fmt.Println(fmt.Sprintf(...)) (but don't forget the newline) (S1038)
  }
  return nil
}

func main() {
  f(nil, []byte(`{"A": "B", "X": "Y"}`)) // main.go:33:14: do not pass a nil Context, even if a function permits it; pass context.TODO if you are unsure about which Context to use (SA1012)
}

ReviewdogにStaticcheckのActionがあるので次のような設定で動き出す。

$ cat .github/workflows/reviewdog.yaml
name: reviewdog
on: [pull_request]
jobs:
  staticcheck:
    name: runner / staticcheck
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: reviewdog/action-staticcheck@v1
        with:
          filter_mode: diff_context
          fail_on_error: true

Reviewdogによるコメント

golangci-lint

golangci-lint複数のlinterをまとめて適用できるツールで、これにStaticcheckも含まれている。

$ brew install golangci-lint
$ golangci-lint --version
golangci-lint has version 1.40.1 built from 625445b on 2021-05-17T08:47:21Z

全てのlinterを有効にしたところほぼすべての行で指摘が入った。 Staticcheckと重複しているものもあるが、デフォルトで有効なdeadcodeによって未使用コードが、errcheckによってエラーハンドリング漏れが検知されている。

$ golangci-lint run --uniq-by-line=false --enable-all main.go 
package main

import (
  "context"
  "encoding/json"
  "fmt"
  "strings"
)

type Color int

const (
  Red Color = 1
  // `Red` is unused (deadcode)
  // exported: exported const Red should have comment (or a comment on this block) or be unexported (revive)
  // SA9004: only the first constant in this group has an explicit type (staticcheck)

  Blue = 2 // `Blue` is unused (deadcode)
)

func f(ctx context.Context, str []byte) error { // `f` - `ctx` is unused (unparam)
  var m map[string]string
  m["A"] = "default" // SA5000: assignment to nil map (staticcheck)
  if err := json.Unmarshal(str, m); err != nil {
    // only one cuddle assignment allowed before if statement (wsl)
    // unmarshal: call of Unmarshal passes non-pointer as second argument (govet)
    // SA1014: json.Unmarshal expects to unmarshal into a pointer, but the provided value is not a pointer (staticcheck)

    return err // error returned from external package is unwrapped: sig: func encoding/json.Unmarshal(data []byte, v interface{}) error (wrapcheck)
  }
  l := *&m
  // assignments should only be cuddled with other assignments (wsl)
  // SA4001: *&x will be simplified to x. It will not copy x. (staticcheck)

  l["A"] = ""
  if strings.ToLower(m["A"]) == strings.ToLower(m["X"]) {
    // only one cuddle assignment allowed before if statement (wsl)
    // SA6005: should use strings.EqualFold instead (staticcheck)

    fmt.Println(fmt.Sprintf("%v -> %s", m, l))
    // use of `fmt.Println` forbidden by pattern `^fmt\.Print(|f|ln)$` (forbidigo)
    // S1038: should use fmt.Printf instead of fmt.Println(fmt.Sprintf(...)) (but don't forget the newline) (gosimple)
  }
  return nil
  // return statements should not be cuddled if block has more than two lines (wsl)
  // return with no blank line before (nlreturn)
}

func main() {
  f(nil, []byte(`{"A": "B", "X": "Y"}`))
  // Error return value is not checked (errcheck)
  // SA1012: do not pass a nil Context, even if a function permits it; pass context.TODO if you are unsure about which Context to use (staticcheck)
}

こちらにもReviewdogのActionがある。 追加で関数の循環的複雑度を測るgocycloをテスト以外で有効にしている。 private repositoryへの依存のためにgit configしているが、GITHUB_TOKENにはrepo scopeがないので別にTokenを発行した。

push時にも実行する際はreporterをgithub-checkにする。ただしCode Suggetionsはサポートしていない。

$ cat .github/workflows/reviewdog.yml
name: reviewdog
on: [pull_request]
jobs:
  golangci-lint:
    name: runner / golangci-lint
    runs-on: ubuntu-latest
    steps:
      - name: git config to get private repositories
        run: git config --global url.'https://${{ secrets.REVIEWDOG_TOKEN }}:[email protected]/sambaiz'.insteadOf 'https://github.com/sambaiz'
      - name: Check out code into the Go module directory
        uses: actions/checkout@v2
      - name: Set up Go
        uses: actions/setup-go@v2
        with:
          go-version: '1.16'
      - name: golangci-lint
        uses: reviewdog/action-golangci-lint@v1
        with:
          reporter: github-pr-review
          filter_mode: diff_context
          fail_on_error: true
  gocyclo:
    name: runner / gocyclo
    runs-on: ubuntu-latest
    steps:
      - name: git config to get private repositories
        run: git config --global url.'https://${{ secrets.REVIEWDOG_TOKEN }}:[email protected]/sambaiz'.insteadOf 'https://github.com/sambaiz'
      - name: Check out code into the Go module directory
        uses: actions/checkout@v2
      - name: Set up Go
        uses: actions/setup-go@v2
        with:
          go-version: '1.16'
      - name: gocyclo
        uses: reviewdog/action-golangci-lint@v1
        with:
          reporter: github-pr-review
          filter_mode: diff_context
          fail_on_error: true
          tool_name: gocyclo
          golangci_lint_flags: "--config=.github/.golangci.yml --disable-all --tests=false -E gocyclo"

$ cat .github/.golangci.yml 
linters-settings:
  gocyclo:
    # min-complexity: 10
    min-complexity: 1

gocycloのチェック

参考

reviewdogによるGoのコードレビュー - DeNA Testing Blog