gopls: wire in LangVersion and ModulePath for gofumpt formatting

If available, pass the LangVersion and ModulePath provided by
go/packages to gofumpt. This allows gofumpt to apply additional
formatting rules that require this information.

Also add a regression test for gofumpt formatting.

Fixes golang/go#51327

Change-Id: I47c8c96d595d62e1c444285ce69ce6a4e61fa74c
Reviewed-on: https://go-review.googlesource.com/c/tools/+/387634
Trust: Robert Findley <rfindley@google.com>
Run-TryBot: Robert Findley <rfindley@google.com>
Trust: Daniel Martí <mvdan@mvdan.cc>
gopls-CI: kokoro <noreply+kokoro@google.com>
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
This commit is contained in:
Robert Findley 2022-02-23 11:34:11 -05:00
parent e6ef770939
commit 5210e0ca15
7 changed files with 111 additions and 15 deletions

View File

@ -22,14 +22,10 @@ func Options(options *source.Options) {
options.ComputeEdits = ComputeEdits
}
options.URLRegexp = relaxedFullWord
options.GofumptFormat = func(ctx context.Context, src []byte) ([]byte, error) {
options.GofumptFormat = func(ctx context.Context, langVersion, modulePath string, src []byte) ([]byte, error) {
return format.Source(src, format.Options{
// TODO: fill LangVersion and ModulePath from the current go.mod.
// The information is availabe as loaded by go/packages via the
// Module type, but it needs to be wired up all the way here.
// We likely want to change the GofumptFormat field type to take
// extra parameters, to then be used in format.Options.
// See https://pkg.go.dev/mvdan.cc/gofumpt@v0.3.0/format#Options.
LangVersion: langVersion,
ModulePath: modulePath,
})
}
updateAnalyzers(options)

View File

@ -301,3 +301,69 @@ func main() {
}
})
}
func TestGofumptFormatting(t *testing.T) {
// Exercise some gofumpt formatting rules:
// - No empty lines following an assignment operator
// - Octal integer literals should use the 0o prefix on modules using Go
// 1.13 and later. Requires LangVersion to be correctly resolved.
// - std imports must be in a separate group at the top. Requires ModulePath
// to be correctly resolved.
const input = `
-- go.mod --
module foo
go 1.17
-- foo.go --
package foo
import (
"foo/bar"
"fmt"
)
const perm = 0755
func foo() {
foo :=
"bar"
fmt.Println(foo, bar.Bar)
}
-- foo.go.formatted --
package foo
import (
"fmt"
"foo/bar"
)
const perm = 0o755
func foo() {
foo := "bar"
fmt.Println(foo, bar.Bar)
}
-- bar/bar.go --
package bar
const Bar = 42
`
WithOptions(
EditorConfig{
Settings: map[string]interface{}{
"gofumpt": true,
},
},
).Run(t, input, func(t *testing.T, env *Env) {
env.OpenFile("foo.go")
env.FormatBuffer("foo.go")
got := env.Editor.BufferText("foo.go")
want := env.ReadWorkspaceFile("foo.go.formatted")
if got != want {
t.Errorf("unexpected formatting result:\n%s", tests.Diff(t, want, got))
}
})
}

View File

@ -56,6 +56,11 @@ func (m *Metadata) PackagePath() string {
return string(m.PkgPath)
}
// ModuleInfo implements the source.Metadata interface.
func (m *Metadata) ModuleInfo() *packages.Module {
return m.Module
}
// KnownMetadata is a wrapper around metadata that tracks its validity.
type KnownMetadata struct {
*Metadata

View File

@ -1025,7 +1025,11 @@ func (s *snapshot) MetadataForFile(ctx context.Context, uri span.URI) ([]source.
var mds []source.Metadata
for _, id := range knownIDs {
md := s.getMetadata(id)
mds = append(mds, md)
// TODO(rfindley): knownIDs and metadata should be in sync, but existing
// code is defensive of nil metadata.
if md != nil {
mds = append(mds, md)
}
}
return mds, nil
}

View File

@ -63,8 +63,24 @@ func Format(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.T
// Apply additional formatting, if any is supported. Currently, the only
// supported additional formatter is gofumpt.
if format := snapshot.View().Options().Hooks.GofumptFormat; snapshot.View().Options().Gofumpt && format != nil {
b, err := format(ctx, buf.Bytes())
if format := snapshot.View().Options().GofumptFormat; snapshot.View().Options().Gofumpt && format != nil {
// gofumpt can customize formatting based on language version and module
// path, if available.
//
// Try to derive this information, but fall-back on the default behavior.
//
// TODO: under which circumstances can we fail to find module information?
// Can this, for example, result in inconsistent formatting across saves,
// due to pending calls to packages.Load?
var langVersion, modulePath string
mds, err := snapshot.MetadataForFile(ctx, fh.URI())
if err == nil && len(mds) > 0 {
if mi := mds[0].ModuleInfo(); mi != nil {
langVersion = mi.GoVersion
modulePath = mi.Path
}
}
b, err := format(ctx, langVersion, modulePath, buf.Bytes())
if err != nil {
return nil, err
}

View File

@ -462,11 +462,16 @@ func (u *UserOptions) SetEnvSlice(env []string) {
// Hooks contains configuration that is provided to the Gopls command by the
// main package.
type Hooks struct {
LicensesText string
GoDiff bool
ComputeEdits diff.ComputeEdits
URLRegexp *regexp.Regexp
GofumptFormat func(ctx context.Context, src []byte) ([]byte, error)
LicensesText string
GoDiff bool
ComputeEdits diff.ComputeEdits
URLRegexp *regexp.Regexp
// GofumptFormat allows the gopls module to wire-in a call to
// gofumpt/format.Source. langVersion and modulePath are used for some
// Gofumpt formatting rules -- see the Gofumpt documentation for details.
GofumptFormat func(ctx context.Context, langVersion, modulePath string, src []byte) ([]byte, error)
DefaultAnalyzers map[string]*Analyzer
TypeErrorAnalyzers map[string]*Analyzer
ConvenienceAnalyzers map[string]*Analyzer

View File

@ -18,6 +18,7 @@ import (
"golang.org/x/mod/modfile"
"golang.org/x/mod/module"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/packages"
"golang.org/x/tools/internal/gocommand"
"golang.org/x/tools/internal/imports"
"golang.org/x/tools/internal/lsp/progress"
@ -319,6 +320,9 @@ type Metadata interface {
// PackagePath is the package path.
PackagePath() string
// ModuleInfo returns the go/packages module information for the given package.
ModuleInfo() *packages.Module
}
// Session represents a single connection from a client.