gopls/internal/analysis/embeddirective: call AddImport directly

This CL causes the embeddirective analyzer to compute the necessary
edits itself, using AddImport, rather than using gopls' hacky
lazy-edits mechanism, which requires gopls's ApplyFix command
to compute the edits later.

Also, support blank imports in AddImport.

Change-Id: Ie51a316503933fbea05ed418ed251f7af9cc624f
Reviewed-on: https://go-review.googlesource.com/c/tools/+/705475
Reviewed-by: Robert Findley <rfindley@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/gopls/internal/analysis/embeddirective/embeddirective.go b/gopls/internal/analysis/embeddirective/embeddirective.go
index 7590cba..58b4a36 100644
--- a/gopls/internal/analysis/embeddirective/embeddirective.go
+++ b/gopls/internal/analysis/embeddirective/embeddirective.go
@@ -26,8 +26,6 @@
 	URL:              "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/embeddirective",
 }
 
-const FixCategory = "addembedimport" // recognized by gopls ApplyFix
-
 func run(pass *analysis.Pass) (any, error) {
 	for _, f := range pass.Files {
 		comments := embedDirectiveComments(f)
@@ -47,16 +45,19 @@
 			pos, end := c.Pos(), c.Pos()+token.Pos(len("//go:embed"))
 
 			if !hasEmbedImport {
-				pass.Report(analysis.Diagnostic{
-					Pos:      pos,
-					End:      end,
-					Message:  `must import "embed" when using go:embed directives`,
-					Category: FixCategory,
-					SuggestedFixes: []analysis.SuggestedFix{{
-						Message: `Add missing "embed" import`,
-						// No TextEdits => computed by a gopls command.
-					}},
-				})
+				// Add blank import of "embed".
+				_, _, edits := analysisinternal.AddImport(pass.TypesInfo, f, "_", "embed", "", c.Pos())
+				if len(edits) > 0 {
+					pass.Report(analysis.Diagnostic{
+						Pos:     pos,
+						End:     end,
+						Message: `must import "embed" when using go:embed directives`,
+						SuggestedFixes: []analysis.SuggestedFix{{
+							Message:   `Add missing "embed" import`,
+							TextEdits: edits,
+						}},
+					})
+				}
 			}
 
 			var msg string
diff --git a/gopls/internal/analysis/embeddirective/testdata/src/a/import_missing.go.golden b/gopls/internal/analysis/embeddirective/testdata/src/a/import_missing.go.golden
new file mode 100644
index 0000000..c2609cb
--- /dev/null
+++ b/gopls/internal/analysis/embeddirective/testdata/src/a/import_missing.go.golden
@@ -0,0 +1,18 @@
+// Copyright 2022 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 a
+
+import (
+	_ "embed"
+	"fmt"
+)
+
+//go:embed embedtext // want "must import \"embed\" when using go:embed directives"
+var s string
+
+// This is main function
+func main() {
+	fmt.Println(s)
+}
diff --git a/gopls/internal/golang/fix.go b/gopls/internal/golang/fix.go
index 0308c38..84ff1a9 100644
--- a/gopls/internal/golang/fix.go
+++ b/gopls/internal/golang/fix.go
@@ -10,7 +10,6 @@
 	"go/token"
 
 	"golang.org/x/tools/go/analysis"
-	"golang.org/x/tools/gopls/internal/analysis/embeddirective"
 	"golang.org/x/tools/gopls/internal/analysis/fillstruct"
 	"golang.org/x/tools/gopls/internal/analysis/unusedparams"
 	"golang.org/x/tools/gopls/internal/cache"
@@ -18,7 +17,6 @@
 	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/protocol"
 	"golang.org/x/tools/gopls/internal/util/bug"
-	"golang.org/x/tools/internal/imports"
 )
 
 // A fixer is a function that suggests a fix for a diagnostic produced
@@ -94,8 +92,7 @@
 	fixers := map[string]fixer{
 		// Fixes for analyzer-provided diagnostics.
 		// These match the Diagnostic.Category.
-		embeddirective.FixCategory: addEmbedImport,
-		fillstruct.FixCategory:     singleFile(fillstruct.SuggestedFix),
+		fillstruct.FixCategory: singleFile(fillstruct.SuggestedFix),
 
 		// Ad-hoc fixers: these are used when the command is
 		// constructed directly by logic in server/code_action.
@@ -183,36 +180,3 @@
 	}
 	return changes, nil
 }
-
-// addEmbedImport adds a missing embed "embed" import with blank name.
-func addEmbedImport(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, _, _ token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) {
-	// Like golang.AddImport, but with _ as Name and using our pgf.
-	protoEdits, err := ComputeImportFixEdits(snapshot.Options().Local, pgf.Src, &imports.ImportFix{
-		StmtInfo: imports.ImportInfo{
-			ImportPath: "embed",
-			Name:       "_",
-		},
-		FixType: imports.AddImport,
-	})
-	if err != nil {
-		return nil, nil, fmt.Errorf("compute edits: %w", err)
-	}
-
-	var edits []analysis.TextEdit
-	for _, e := range protoEdits {
-		start, end, err := pgf.RangePos(e.Range)
-		if err != nil {
-			return nil, nil, err // e.g. invalid range
-		}
-		edits = append(edits, analysis.TextEdit{
-			Pos:     start,
-			End:     end,
-			NewText: []byte(e.NewText),
-		})
-	}
-
-	return pkg.FileSet(), &analysis.SuggestedFix{
-		Message:   "Add embed import",
-		TextEdits: edits,
-	}, nil
-}
diff --git a/internal/analysisinternal/analysis.go b/internal/analysisinternal/analysis.go
index bc7f998..cea89d3 100644
--- a/internal/analysisinternal/analysis.go
+++ b/internal/analysisinternal/analysis.go
@@ -193,18 +193,23 @@
 	return fmt.Errorf("Pass.ReadFile: %s is not among OtherFiles, IgnoredFiles, or names of Files", filename)
 }
 
-// AddImport checks whether this file already imports pkgpath and
-// that import is in scope at pos. If so, it returns the name under
-// which it was imported and a zero edit. Otherwise, it adds a new
-// import of pkgpath, using a name derived from the preferred name,
-// and returns the chosen name, a prefix to be concatenated with member
-// to form a qualified name, and the edit for the new import.
+// AddImport checks whether this file already imports pkgpath and that
+// the import is in scope at pos. If so, it returns the name under
+// which it was imported and no edits. Otherwise, it adds a new import
+// of pkgpath, using a name derived from the preferred name, and
+// returns the chosen name, a prefix to be concatenated with member to
+// form a qualified name, and the edit for the new import.
 //
-// In the special case that pkgpath is dot-imported then member, the
-// identifier for which the import is being added, is consulted. If
-// member is not shadowed at pos, AddImport returns (".", "", nil).
-// (AddImport accepts the caller's implicit claim that the imported
-// package declares member.)
+// The member argument indicates the name of the desired symbol within
+// the imported package. This is needed in the case when the existing
+// import is a dot import, because then it is possible that the
+// desired symbol is shadowed by other declarations in the current
+// package. If member is not shadowed at pos, AddImport returns (".",
+// "", nil). (AddImport accepts the caller's implicit claim that the
+// imported package declares member.)
+//
+// Use a preferredName of "_" to request a blank import;
+// member is ignored in this case.
 //
 // It does not mutate its arguments.
 func AddImport(info *types.Info, file *ast.File, preferredName, pkgpath, member string, pos token.Pos) (name, prefix string, newImport []analysis.TextEdit) {
@@ -220,6 +225,10 @@
 		pkgname := info.PkgNameOf(spec)
 		if pkgname != nil && pkgname.Imported().Path() == pkgpath {
 			name = pkgname.Name()
+			if preferredName == "_" {
+				// Request for blank import; any existing import will do.
+				return name, "", nil
+			}
 			if name == "." {
 				// The scope of ident must be the file scope.
 				if s, _ := scope.LookupParent(member, pos); s == info.Scopes[file] {
@@ -232,8 +241,12 @@
 	}
 
 	// We must add a new import.
+
 	// Ensure we have a fresh name.
-	newName := FreshName(scope, pos, preferredName)
+	newName := preferredName
+	if preferredName != "_" {
+		newName = FreshName(scope, pos, preferredName)
+	}
 
 	// Create a new import declaration either before the first existing
 	// declaration (which must exist), including its comments; or
@@ -246,6 +259,7 @@
 	if newName != preferredName || newName != pathpkg.Base(pkgpath) {
 		newText = fmt.Sprintf("%s %q", newName, pkgpath)
 	}
+
 	decl0 := file.Decls[0]
 	var before ast.Node = decl0
 	switch decl0 := decl0.(type) {