go/analysis/passes/modernize: add importcomment analyzer

The new analyzer removes canonical import path comments, such as

	package foo // import "example.com/foo"

The go command enforced these comments in GOPATH mode via "go get", but
ignores them once a package belongs to a module, so they are obsolete in
module-based projects. The fix deletes the comment.

The analyzer only runs when pass.Module is set, matching the agreed
behavior of removing the comment when a go.mod is present without
inspecting the import path.

Fixes golang/go#80049

Change-Id: I0ad4eb113496a2fba03eb1052e615af3f804a347
GitHub-Last-Rev: dbfa397f47d50fb487e38cacbe6c78da684b26ea
GitHub-Pull-Request: golang/tools#655
Reviewed-on: https://go-review.googlesource.com/c/tools/+/796860
Auto-Submit: Alan Donovan <adonovan@google.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
Reviewed-by: Madeline Kalil <mkalil@google.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/go/analysis/passes/modernize/doc.go b/go/analysis/passes/modernize/doc.go
index 260dd50..c5545c0 100644
--- a/go/analysis/passes/modernize/doc.go
+++ b/go/analysis/passes/modernize/doc.go
@@ -182,6 +182,19 @@
 
 This fix only applies to `range` loops.
 
+# Analyzer importcomment
+
+importcomment: remove obsolete comments specifying canonical import path
+
+The importcomment analyzer removes comments specifying the canonical
+import path, such as
+
+	package foo // import "example.com/foo"
+
+The go command enforced these comments in GOPATH mode via "go get", but
+ignores them in module mode, so they are obsolete once the package
+belongs to a module. The fix removes the comment.
+
 # Analyzer mapsloop
 
 mapsloop: replace explicit loops over maps with calls to maps package
diff --git a/go/analysis/passes/modernize/export_test.go b/go/analysis/passes/modernize/export_test.go
index 0823331..0401cf0 100644
--- a/go/analysis/passes/modernize/export_test.go
+++ b/go/analysis/passes/modernize/export_test.go
@@ -7,6 +7,7 @@
 package modernize
 
 var (
+	ImportCommentAnalyzer  = importCommentAnalyzer
 	SlicesBackwardAnalyzer = slicesBackwardAnalyzer
 	UnsafeFuncsAnalyzer    = unsafeFuncsAnalyzer
 )
diff --git a/go/analysis/passes/modernize/importcomment.go b/go/analysis/passes/modernize/importcomment.go
new file mode 100644
index 0000000..1538783
--- /dev/null
+++ b/go/analysis/passes/modernize/importcomment.go
@@ -0,0 +1,69 @@
+// 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 modernize
+
+import (
+	"strings"
+
+	"golang.org/x/tools/go/analysis"
+	"golang.org/x/tools/internal/analysis/analyzerutil"
+)
+
+var importCommentAnalyzer = &analysis.Analyzer{
+	Name: "importcomment",
+	Doc:  analyzerutil.MustExtractDoc(doc, "importcomment"),
+	URL:  "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#importcomment",
+	Run:  importcomment,
+}
+
+func importcomment(pass *analysis.Pass) (any, error) {
+	// Import path comments are ignored in module mode.
+	if pass.Module == nil {
+		return nil, nil
+	}
+
+	for _, file := range pass.Files {
+		// An import comment follows the package name on the same line.
+		pkgEnd := file.Name.End()
+		pkgLine := pass.Fset.Position(pkgEnd).Line
+		for _, c := range file.Comments {
+			if len(c.List) != 1 {
+				continue
+			}
+			if c.Pos() < pkgEnd {
+				continue
+			}
+			commentLine := pass.Fset.Position(c.Pos()).Line
+			if commentLine > pkgLine {
+				break // comments are sorted; the rest are on later lines
+			}
+			// Have: package p // comment
+			if !isImportComment(c.Text()) {
+				continue
+			}
+			pass.Report(analysis.Diagnostic{
+				Pos:     c.Pos(),
+				End:     c.End(),
+				Message: "canonical import path comment is ignored in module mode",
+				SuggestedFixes: []analysis.SuggestedFix{{
+					Message: "Remove obsolete import path comment",
+					TextEdits: []analysis.TextEdit{{
+						Pos: pkgEnd, // deletes the preceding space too
+						End: c.End(),
+					}},
+				}},
+			})
+		}
+	}
+
+	return nil, nil
+}
+
+// isImportComment reports whether text, a comment's content with its
+// markers removed, is a canonical import path comment, import "path".
+func isImportComment(text string) bool {
+	text = strings.TrimSpace(text)
+	return strings.HasPrefix(text, `import "`) && strings.HasSuffix(text, `"`)
+}
diff --git a/go/analysis/passes/modernize/modernize.go b/go/analysis/passes/modernize/modernize.go
index d7dfe55..23dd8ab 100644
--- a/go/analysis/passes/modernize/modernize.go
+++ b/go/analysis/passes/modernize/modernize.go
@@ -38,6 +38,7 @@
 	EmbedLitAnalyzer,
 	ErrorsAsTypeAnalyzer,
 	ForVarAnalyzer,
+	importCommentAnalyzer, // awaiting public symbol
 	MapsLoopAnalyzer,
 	MinMaxAnalyzer,
 	NewExprAnalyzer,
diff --git a/go/analysis/passes/modernize/modernize_test.go b/go/analysis/passes/modernize/modernize_test.go
index bc8ecfb..bb2f7ac 100644
--- a/go/analysis/passes/modernize/modernize_test.go
+++ b/go/analysis/passes/modernize/modernize_test.go
@@ -5,6 +5,7 @@
 package modernize_test
 
 import (
+	"path/filepath"
 	"testing"
 
 	. "golang.org/x/tools/go/analysis/analysistest"
@@ -86,6 +87,13 @@
 	RunWithSuggestedFixes(t, TestData(), modernize.PlusBuildAnalyzer, "plusbuild")
 }
 
+func TestImportComment(t *testing.T) {
+	// Loaded in module mode (a go.mod at the testdata root) so that
+	// pass.Module is set, which the analyzer requires.
+	dir := filepath.Join(TestData(), "importcommentmod")
+	RunWithSuggestedFixes(t, dir, modernize.ImportCommentAnalyzer, "./...")
+}
+
 func TestReflectTypeFor(t *testing.T) {
 	testenv.NeedsGo1Point(t, 25) // requires go1.25 types.Var.Kind
 	RunWithSuggestedFixes(t, TestData(), modernize.ReflectTypeForAnalyzer, "reflecttypefor")
diff --git a/go/analysis/passes/modernize/testdata/importcommentmod/block.go b/go/analysis/passes/modernize/testdata/importcommentmod/block.go
new file mode 100644
index 0000000..654039d
--- /dev/null
+++ b/go/analysis/passes/modernize/testdata/importcommentmod/block.go
@@ -0,0 +1,5 @@
+// want +2 `canonical import path comment is ignored in module mode`
+
+package importcommentmod /* import "example.com/importcommentmod" */
+
+func block() {}
diff --git a/go/analysis/passes/modernize/testdata/importcommentmod/block.go.golden b/go/analysis/passes/modernize/testdata/importcommentmod/block.go.golden
new file mode 100644
index 0000000..dea04ac
--- /dev/null
+++ b/go/analysis/passes/modernize/testdata/importcommentmod/block.go.golden
@@ -0,0 +1,5 @@
+// want +2 `canonical import path comment is ignored in module mode`
+
+package importcommentmod
+
+func block() {}
diff --git a/go/analysis/passes/modernize/testdata/importcommentmod/go.mod b/go/analysis/passes/modernize/testdata/importcommentmod/go.mod
new file mode 100644
index 0000000..32f9f97
--- /dev/null
+++ b/go/analysis/passes/modernize/testdata/importcommentmod/go.mod
@@ -0,0 +1,3 @@
+module example.com/importcommentmod
+
+go 1.24
diff --git a/go/analysis/passes/modernize/testdata/importcommentmod/importcomment.go b/go/analysis/passes/modernize/testdata/importcommentmod/importcomment.go
new file mode 100644
index 0000000..b92c8f8
--- /dev/null
+++ b/go/analysis/passes/modernize/testdata/importcommentmod/importcomment.go
@@ -0,0 +1,7 @@
+// want +2 `canonical import path comment is ignored in module mode`
+
+package importcommentmod // import "example.com/importcommentmod"
+
+import "fmt"
+
+var _ = fmt.Sprint
diff --git a/go/analysis/passes/modernize/testdata/importcommentmod/importcomment.go.golden b/go/analysis/passes/modernize/testdata/importcommentmod/importcomment.go.golden
new file mode 100644
index 0000000..029a680
--- /dev/null
+++ b/go/analysis/passes/modernize/testdata/importcommentmod/importcomment.go.golden
@@ -0,0 +1,7 @@
+// want +2 `canonical import path comment is ignored in module mode`
+
+package importcommentmod
+
+import "fmt"
+
+var _ = fmt.Sprint
diff --git a/go/analysis/passes/modernize/testdata/importcommentmod/other.go b/go/analysis/passes/modernize/testdata/importcommentmod/other.go
new file mode 100644
index 0000000..26c5010
--- /dev/null
+++ b/go/analysis/passes/modernize/testdata/importcommentmod/other.go
@@ -0,0 +1,3 @@
+package importcommentmod // regular comment, not an import path
+
+func other() {}
diff --git a/gopls/doc/analyzers.md b/gopls/doc/analyzers.md
index 10f829c..70d2b91 100644
--- a/gopls/doc/analyzers.md
+++ b/gopls/doc/analyzers.md
@@ -3320,6 +3320,20 @@
 
 Package documentation: [ifaceassert](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/ifaceassert)
 
+<a id='importcomment'></a>
+## `importcomment`: remove obsolete comments specifying canonical import path
+
+The importcomment analyzer removes comments specifying the canonical import path, such as
+
+	package foo // import "example.com/foo"
+
+The go command enforced these comments in GOPATH mode via "go get", but ignores them in module mode, so they are obsolete once the package belongs to a module. The fix removes the comment.
+
+
+Default: on.
+
+Package documentation: [importcomment](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#importcomment)
+
 <a id='infertypeargs'></a>
 ## `infertypeargs`: check for unnecessary type arguments in call expressions
 
diff --git a/gopls/internal/doc/api.json b/gopls/internal/doc/api.json
index b9cf154..24efa51 100644
--- a/gopls/internal/doc/api.json
+++ b/gopls/internal/doc/api.json
@@ -1527,6 +1527,12 @@
 							"Status": ""
 						},
 						{
+							"Name": "\"importcomment\"",
+							"Doc": "remove obsolete comments specifying canonical import path\n\nThe importcomment analyzer removes comments specifying the canonical\nimport path, such as\n\n\tpackage foo // import \"example.com/foo\"\n\nThe go command enforced these comments in GOPATH mode via \"go get\", but\nignores them in module mode, so they are obsolete once the package\nbelongs to a module. The fix removes the comment.",
+							"Default": "true",
+							"Status": ""
+						},
+						{
 							"Name": "\"infertypeargs\"",
 							"Doc": "check for unnecessary type arguments in call expressions\n\nExplicit type arguments may be omitted from call expressions if they can be\ninferred from function arguments, or from other type arguments:\n\n\tfunc f[T any](T) {}\n\t\n\tfunc _() {\n\t\tf[string](\"foo\") // string could be inferred\n\t}\n",
 							"Default": "true",
@@ -3542,6 +3548,12 @@
 			"Default": true
 		},
 		{
+			"Name": "importcomment",
+			"Doc": "remove obsolete comments specifying canonical import path\n\nThe importcomment analyzer removes comments specifying the canonical\nimport path, such as\n\n\tpackage foo // import \"example.com/foo\"\n\nThe go command enforced these comments in GOPATH mode via \"go get\", but\nignores them in module mode, so they are obsolete once the package\nbelongs to a module. The fix removes the comment.",
+			"URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#importcomment",
+			"Default": true
+		},
+		{
 			"Name": "infertypeargs",
 			"Doc": "check for unnecessary type arguments in call expressions\n\nExplicit type arguments may be omitted from call expressions if they can be\ninferred from function arguments, or from other type arguments:\n\n\tfunc f[T any](T) {}\n\t\n\tfunc _() {\n\t\tf[string](\"foo\") // string could be inferred\n\t}\n",
 			"URL": "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/infertypeargs",