Skip to content

goplsのCode Action「declare missing method of INTERFACE」はアルファベット順に定義される

TL;DR

gopls のクイックフィックス **“Declare missing methods of iface”**が生成するスタブは、インターフェースのメソッドを unique Idでソートした順に並ぶ。

結果として、大文字で始まるエクスポート済みメソッドが先、次に小文字のメソッドが名前順で出力される。宣言順にはならない。

Diagnostics に出たエラー

次のようにハンドラを定義したとします。

go
package handler

import (
	"github.com/labstack/echo/v4"
	"github.com/kazugor/app/domain/service"
)

type IUserHandler interface {
	LoadByID(c echo.Context) error
	Create(c echo.Context) error
	Update(c echo.Context) error
	Delete(c echo.Context) error
}

type UserHandler struct {
	service.IUserService
}

func NewUserHandler(srv service.IUserService) IUserHandler {
	return &UserHandler{srv}
}

しかし UserHandler がインターフェースを満たしていないため、gopls の Diagnostics に下記のようなエラーが出ます。

go
cannot use &UserHandler{…} (value of type *UserHandler) as IUserHandler value in return statement:
*UserHandler does not implement IUserHandler (wrong type for method Create)
    have Create(*dto.UserModel) error
    want Create(echo.Context) error [InvalidIfaceAssign]

ここでCode Action “Declare missing methods of iface” を実行すると、UserHandler に不足したメソッドのスタブが自動生成される。

今回はIUserServiceが以下のように定義されているので、

go
type IUserService interface {
	LoadByID(id uint) (*dto.UserModel, error)
	Create(user *dto.UserModel) error
	Update(user *dto.UserModel) error
	Delete(id uint) error
}

ここで生成されるスタブは宣言順に定義されるかと思いきや、実際には次のように アルファベット順 で出力されました。

go
// Create implements IUserHandler.
// Subtle: this method shadows the method (IUserService).Create of UserHandler.IUserService.
func (u *UserHandler) Create(c echo.Context) error {
	panic("unimplemented")
}

// Delete implements IUserHandler.
// Subtle: this method shadows the method (IUserService).Delete of UserHandler.IUserService.
func (u *UserHandler) Delete(c echo.Context) error {
	panic("unimplemented")
}

// LoadByID implements IUserHandler.
// Subtle: this method shadows the method (IUserService).LoadByID of UserHandler.IUserService.
func (u *UserHandler) LoadByID(c echo.Context) error {
	panic("unimplemented")
}

// Update implements IUserHandler.
// Subtle: this method shadows the method (IUserService).Update of UserHandler.IUserService.
func (u *UserHandler) Update(c echo.Context) error {
	panic("unimplemented")
}

なぜ?という話。

go toolsを覗く

ここでは Declare missing methods of iface のCode Actionの実行フローを、golang/toolsの内部実装をもとに追っていきます。

流れは「型チェックでエラー収集 → Diagnostic 生成・表示 → Code Action → コマンド実行 → スタブ生成」と段階的に進みます。

1. 型チェックで “missing method” エラーを記録

missing methodのtypes.Errorは下記のコードで記録されます。

エラー群は typeErrorsToDiagnostics で LSP の Diagnostic に変換されます。

2. Diagnostics の集約と Publish

Snapshot.PackageDiagnostics がパッケージを走査し、上で得た Diagnostic を URI 単位に集約します。

サーバは diagnoseChangedFiles でこれらを取得して LSP クライアントへ。

3. Code Action の提示

クライアントからの textDocument/codeAction は server.CodeAction で処理され、golang.CodeActions へ。

golang.CodeActions の quickFix ルーチンがタイプエラーを精査し、”missing method” 系のメッセージに対して stubmethods.GetIfaceStubInfo() を呼び出し、Fix コマンドを登録します。

ここで addApplyFixAction が ApplyFix コマンド (fixMissingInterfaceMethods) を仕込み、クライアントに“Declare missing methods of …” が表示されます。

4. Fix コマンド実行と編集取得

アクション選択で gopls.apply_fix が起動。サーバ側 commandHandler.ApplyFix は対応する fixer を実行し、編集を取得。

golang.ApplyFix は fix ID を stubMissingInterfaceMethodsFixer にマップし、最も狭いパッケージ&レンジを求めて SuggestedFix → LSP TextEdit に変換します。

5. スタブ生成ロジック

stubMissingInterfaceMethodsFixer は stubmethods.GetIfaceStubInfo で具体型・インターフェース情報を抽出し、insertDeclsAfter に emitter を渡します。

insertDeclsAfter が対象ファイルを再構築し、適切な import を調整しつつメソッド宣言を挿入する。

実際のメソッド本文生成は IfaceStubInfo.Emit にまとまっており、go/types が提供する順序で足りないメソッドを列挙、panic スタブを出力します。

6. go/types によるメソッド順序付け

IfaceStubInfo.Emit が ifaceType.NumMethods() をそのまま走査するため、go/types.Interface が提供する順番=スタブ生成順になります。

Interface.Method の実体は typeSet().Method(i) で、コメントに “The methods are ordered by their unique Id.” と明記されています。

typeSet() の構築過程(computeInterfaceTypeSet)で allMethods を集約し、sortMethods(allMethods) で並べ替えてから ityp.tset.methods に格納します。

sortMethods は内部 object 型の cmp を使って slices.SortFunc でソート。

object.cmp は “nil → 非 nil → エクスポート → 非エクスポート → 名前 → (非エクスポートの場合)パッケージパス” で比較する実装で、これが “unique Id”順の正体です。

unique Id 自体は Id(pkg, name) を指し、エクスポート名はそのまま、非エクスポート名は pkg.path + "." + name で一意化。これにより上記 cmp の規則と一致します。

結果

以上より、gopls の “Declare missing methods of INTERFACE” は go/types の型情報に基づき、インターフェースのメソッドを unique Id 順にソートしてスタブを生成することが分かりました。