diff --git a/go/analysis/passes/gofmt/doc.go b/go/analysis/passes/gofmt/doc.go new file mode 100644 index 00000000000..59dd9add266 --- /dev/null +++ b/go/analysis/passes/gofmt/doc.go @@ -0,0 +1,15 @@ +// Copyright 2026 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package gofmt defines an Analyzer that reports lines that would be +// changed by gofmt. +// +// # Analyzer gofmt +// +// gofmt: report lines whose formatting differs from gofmt's +// +// This analyzer reports diagnostic warnings for lines in Go source files +// that are not formatted according to gofmt. Each diagnostic includes a +// suggested fix to apply the correct formatting. +package gofmt diff --git a/go/analysis/passes/gofmt/gofmt.go b/go/analysis/passes/gofmt/gofmt.go new file mode 100644 index 00000000000..276701e6cec --- /dev/null +++ b/go/analysis/passes/gofmt/gofmt.go @@ -0,0 +1,70 @@ +// Copyright 2026 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package gofmt + +import ( + "bytes" + _ "embed" + "go/ast" + "go/format" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/internal/analysis/analyzerutil" + "golang.org/x/tools/internal/diff" +) + +//go:embed doc.go +var doc string + +var Analyzer = &analysis.Analyzer{ + Name: "gofmt", + Doc: analyzerutil.MustExtractDoc(doc, "gofmt"), + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/gofmt", + Run: run, + RunDespiteErrors: true, +} + +func run(pass *analysis.Pass) (any, error) { + for _, file := range pass.Files { + if ast.IsGenerated(file) { + continue + } + tokenFile := pass.Fset.File(file.FileStart) + filename := tokenFile.Name() + + src, err := pass.ReadFile(filename) + if err != nil { + continue + } + + formatted, err := format.Source(src) + if err != nil { + continue + } + + if bytes.Equal(src, formatted) { + continue + } + + for _, edit := range diff.Lines(string(src), string(formatted)) { + pos := tokenFile.Pos(edit.Start) + end := tokenFile.Pos(edit.End) + pass.Report(analysis.Diagnostic{ + Pos: pos, + End: end, + Message: "file not formatted correctly", + SuggestedFixes: []analysis.SuggestedFix{{ + Message: "Format with gofmt", + TextEdits: []analysis.TextEdit{{ + Pos: pos, + End: end, + NewText: []byte(edit.New), + }}, + }}, + }) + } + } + return nil, nil +} diff --git a/go/analysis/passes/gofmt/gofmt_test.go b/go/analysis/passes/gofmt/gofmt_test.go new file mode 100644 index 00000000000..b31e83e3ee8 --- /dev/null +++ b/go/analysis/passes/gofmt/gofmt_test.go @@ -0,0 +1,17 @@ +// Copyright 2026 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package gofmt_test + +import ( + "testing" + + "golang.org/x/tools/go/analysis/analysistest" + "golang.org/x/tools/go/analysis/passes/gofmt" +) + +func Test(t *testing.T) { + testdata := analysistest.TestData() + analysistest.RunWithSuggestedFixes(t, testdata, gofmt.Analyzer, "a") +} diff --git a/go/analysis/passes/gofmt/testdata/src/a/a.go b/go/analysis/passes/gofmt/testdata/src/a/a.go new file mode 100644 index 00000000000..c0eac13e696 --- /dev/null +++ b/go/analysis/passes/gofmt/testdata/src/a/a.go @@ -0,0 +1,9 @@ +package a + +import "fmt" // want "file not formatted correctly" + +func f( ) { // want "file not formatted correctly" + + x:=1 // want "file not formatted correctly" + fmt.Println(x) +} diff --git a/go/analysis/passes/gofmt/testdata/src/a/a.go.golden b/go/analysis/passes/gofmt/testdata/src/a/a.go.golden new file mode 100644 index 00000000000..b719c575b48 --- /dev/null +++ b/go/analysis/passes/gofmt/testdata/src/a/a.go.golden @@ -0,0 +1,9 @@ +package a + +import "fmt" // want "file not formatted correctly" + +func f() { // want "file not formatted correctly" + + x := 1 // want "file not formatted correctly" + fmt.Println(x) +} diff --git a/gopls/doc/analyzers.md b/gopls/doc/analyzers.md index 8cbe47edae9..556e247667c 100644 --- a/gopls/doc/analyzers.md +++ b/gopls/doc/analyzers.md @@ -3277,6 +3277,16 @@ Default: on. Package documentation: [framepointer](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/framepointer) + +## `gofmt`: report lines whose formatting differs from gofmt's + +This analyzer reports diagnostic warnings for lines in Go source files that are not formatted according to gofmt. Each diagnostic includes a suggested fix to apply the correct formatting. + + +Default: off. Enable by setting `"analyses": {"gofmt": true}`. + +Package documentation: [gofmt](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/gofmt) + ## `hostport`: check format of addresses passed to net.Dial diff --git a/gopls/internal/doc/api.json b/gopls/internal/doc/api.json index 290c900b5c3..6847a90cb87 100644 --- a/gopls/internal/doc/api.json +++ b/gopls/internal/doc/api.json @@ -1508,6 +1508,12 @@ "Default": "true", "Status": "" }, + { + "Name": "\"gofmt\"", + "Doc": "report lines whose formatting differs from gofmt's\n\nThis analyzer reports diagnostic warnings for lines in Go source files\nthat are not formatted according to gofmt. Each diagnostic includes a\nsuggested fix to apply the correct formatting.", + "Default": "false", + "Status": "" + }, { "Name": "\"hostport\"", "Doc": "check format of addresses passed to net.Dial\n\nThis analyzer flags code that produce network address strings using\nfmt.Sprintf, as in this example:\n\n addr := fmt.Sprintf(\"%s:%d\", host, 12345) // \"will not work with IPv6\"\n ...\n conn, err := net.Dial(\"tcp\", addr) // \"when passed to dial here\"\n\nThe analyzer suggests a fix to use the correct approach, a call to\nnet.JoinHostPort:\n\n addr := net.JoinHostPort(host, \"12345\")\n ...\n conn, err := net.Dial(\"tcp\", addr)\n\nA similar diagnostic and fix are produced for a format string of \"%s:%s\".\n", @@ -3509,6 +3515,12 @@ "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/framepointer", "Default": true }, + { + "Name": "gofmt", + "Doc": "report lines whose formatting differs from gofmt's\n\nThis analyzer reports diagnostic warnings for lines in Go source files\nthat are not formatted according to gofmt. Each diagnostic includes a\nsuggested fix to apply the correct formatting.", + "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/gofmt", + "Default": false + }, { "Name": "hostport", "Doc": "check format of addresses passed to net.Dial\n\nThis analyzer flags code that produce network address strings using\nfmt.Sprintf, as in this example:\n\n addr := fmt.Sprintf(\"%s:%d\", host, 12345) // \"will not work with IPv6\"\n ...\n conn, err := net.Dial(\"tcp\", addr) // \"when passed to dial here\"\n\nThe analyzer suggests a fix to use the correct approach, a call to\nnet.JoinHostPort:\n\n addr := net.JoinHostPort(host, \"12345\")\n ...\n conn, err := net.Dial(\"tcp\", addr)\n\nA similar diagnostic and fix are produced for a format string of \"%s:%s\".\n", diff --git a/gopls/internal/settings/analysis.go b/gopls/internal/settings/analysis.go index 247d7b70a82..6edf028ae6d 100644 --- a/gopls/internal/settings/analysis.go +++ b/gopls/internal/settings/analysis.go @@ -11,6 +11,7 @@ import ( "golang.org/x/tools/go/analysis/passes/atomicalign" "golang.org/x/tools/go/analysis/passes/deepequalerrors" "golang.org/x/tools/go/analysis/passes/fieldalignment" + "golang.org/x/tools/go/analysis/passes/gofmt" "golang.org/x/tools/go/analysis/passes/inline" "golang.org/x/tools/go/analysis/passes/modernize" "golang.org/x/tools/go/analysis/passes/nilness" @@ -176,6 +177,9 @@ func initAnalyzers() (res []*Analyzer) { {analyzer: errorsastypeshadow.Analyzer}, // under evaluation {analyzer: writestring.Analyzer}, // under evaluation + // disabled by default + {analyzer: gofmt.Analyzer, severity: protocol.SeverityInformation, nonDefault: true}, + // disabled due to high false positives {analyzer: shadow.Analyzer, severity: protocol.SeverityHint, nonDefault: true}, // very noisy {analyzer: fieldalignment.Analyzer, severity: protocol.SeverityHint, nonDefault: true}, // #67762, #76237