GoのimportのソートをgoimportsよりもちゃんとキレイにするLint兼変換ツールを作った


# はじめに

普段の開発でもそこそこ困っていたGoのimportのソートをいい感じにしてくれるLint兼変換ツールを作りました。

作成したツールはstrictgoimportsという名前でGitHubリンクは以下になります。

https://github.com/momotaro98/strictgoimports

# [課題] Goコミュニティで現時点(2020-12-14)で未だに収拾がついていない[goimports](https://pkg.go.dev/golang.org/x/tools/cmd/goimports)でのソートにおけるIssue

具体的に https://github.com/golang/go/issues/20818 のIssueであがっている内容ですが現時点(2020-12-14)でCloseになっていません。

## 課題の概要

#### Input

対象ファイルが以下のようになってしまっている場合があるとします。私が普段利用しているIDEの機能では以下のようになることがよくあります。

```go
import (
	"testing"

	"github.com/Sirupsen/logrus"
	"context"
)
```

#### As Is

現状の`x/tools/cmd/goimports`の出力はこのようになってしまいます。

```go
import (
	"testing"

	"context"

	"github.com/Sirupsen/logrus"
)
```

#### Expected

しかし`"testing"`と`"context"`は共にGoの標準パッケージという同じカテゴリなので以下が望ましいです。

```go
import (
	"context"
	"testing"

	"github.com/Sirupsen/logrus"
)
```

## なかなか解決しない理由 → "決めの問題"がいくつかあるから

なかなかIssueが解決しないのは、`import`のソートとグルーピングの仕様を(今更)決める必要があるからです。Issueを見ると、そもそもこのままでいいじゃないかという意見もあったりします。

私が把握している中では大きく以下の2つの必要な"決め"があります。

#### `import`内のコメントどうするか

```go
import (
	"github.com/Sirupsen/logrus"

	// ソート&グルーピングしたとき
	/*
		ここのコメントは
		どうなるだろう？
	*/
	"testing"
)
```

#### cgoではimportが特殊になるがどうするか

```go
/*
#include <math.h>
#cgo LDFLAGS: -lm
*/
import "C"
import (
	"os"
)
```

### どうするか → 自分で仕様決めて作るしかない

厳密にちゃんとしてなくてもいいので指摘&修正するツールを自分で作ることにしました。

# strictgoimports ができること

## Installation

```
$ go get -u github.com/momotaro98/strictgoimports/cmd/strictgoimports
```

## Lintとしての機能

Lintツールとして下記の例のように、指定したファイル(or ディレクトリ)に対して、先頭の修正箇所と、あるべきソート順を出力してくれます。

input file

```go:sample.go
package src

import (
	_ "fmt"
	_ "github.com/golang/mock/gomock"
	_ "github.com/momotaro98/strictgoimports"
)
```

result

```
$ strictgoimports -local "github.com/momotaro98" sample.go
sample.go:7:2: import not sorted correctly. should be replace to
import (
        _ "fmt"

        _ "github.com/golang/mock/gomock"

        _ "github.com/momotaro98/strictgoimports"
)
```

`-local`というオプションは[goimports](https://pkg.go.dev/golang.org/x/tools/cmd/goimports)と同様にローカルパッケージのPathを指定するときに使うもので上記のように標準パッケージ、サードパーティパッケージ、ローカルパッケージの3グループとしてくくられます。

## 変換ツールとしての機能

goimportsと同様に`-w`オプションをつけることでLintで指摘する内容として上書き修正します。

その他のオプションとして`-exclude`や`-exclude-dir`を指定することで対象外にしたいファイルやディレクトリを指定することができます。

## イレギュラーなケース

### `//`なインラインコメントのみがある場合

すべてのコメントをimportスコープの1番上まで引き上げます。

input file

```go:inner_comment.go
package src

import (
	_ "strings"

	_ "github.com/golang/mock/gomock"
	_ "github.com/momotaro98/strictgoimports"

	// comment
	// my favorite comment
	_ "fmt"
)
```

result

```
inner_comment.go:4:2: import not sorted correctly. should be replace to
import (
        // comment
        // my favorite comment
        _ "fmt"
        _ "strings"

        _ "github.com/golang/mock/gomock"
        _ "github.com/momotaro98/strictgoimports"
)
```

### `/* */`なコメントがある場合

これに関しては、良い実装がわかっておらず現時点(2020-12-14)でimportのスコープ内に`/* */`が存在した時点でエラーにするようにしています。もう少し精進すれば対応できそうですが、import内にコメントを`/* */`で書くのは見たことが無いのでまあこのままでも良さそうとも思っています。

input file

```go:star_comment.go
package src

import (
	/*
		comment
	*/_ "strings"

	_ "fmt"

	_ "github.com/golang/mock/gomock"
)
```

```
$ strictgoimports star_comment.go
Error: Message: there's star comment (/* */) in import lines. Position: star_comment.go:4:2
```

### cgoのimportがあるとき

`import "C"`は特別なimportとして完全に無視して扱うようにしています。

input

```go:cgo.go
package src

/*
#include <math.h>
#cgo LDFLAGS: -lm
*/
import "C"
import (
	_ "fmt"

	_ "github.com/golang/mock/gomock"
	_ "github.com/momotaro98/strictgoimports"
)
```

result

```
$ strictgoimports cgo.go

```

(↑修正箇所が無いので何も出力されません。)

# おわりに

[golangci-lint](https://github.com/golangci/golangci-lint)に追加されたいという思いもあるのですが、オレオレ仕様で決めて作ってしまっている、かつ、公式でも今後動きがある"かも"しれないので仲間入りしづらいという感じです。

ひとまず作ってみたという感じがあり、現状(2020-12-14)ではパフォーマンスがあまり良くないことがわかっています。分析して修正する予定です。