internal/lsp/analysis: add quickfix for "no new vars on left side"

This change adds a quick fix for type errors of the type "no new vars on left side of :=". It will replace the ":=" with an "=".

Updates golang/go#34644

Change-Id: I91af8eb82956104229c3b4f3d0fce60fdfdbb5ea
Reviewed-on: https://go-review.googlesource.com/c/tools/+/225477
Run-TryBot: Rohan Challa <rohan@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/gopls/doc/analyzers.md b/gopls/doc/analyzers.md
index c2d8498..47a65cf 100644
--- a/gopls/doc/analyzers.md
+++ b/gopls/doc/analyzers.md
@@ -321,6 +321,24 @@
 
 Default value: `true`.
 
+### **nonewvars**
+
+suggested fixes for "no new vars on left side of :="
+
+This checker provides suggested fixes for type errors of the
+type "no new vars on left side of :=". For example:
+```go
+z := 1
+z := 2
+```
+will turn into
+```go
+z := 1
+z = 2
+```
+
+Default value: `true`.
+
 ### **noresultvalues**
 
 suggested fixes for "no result values expected"
diff --git a/internal/analysisinternal/analysis.go b/internal/analysisinternal/analysis.go
index 1599702..39f4bb6 100644
--- a/internal/analysisinternal/analysis.go
+++ b/internal/analysisinternal/analysis.go
@@ -29,6 +29,7 @@
 type TypeErrorPass string
 
 const (
+	NoNewVars      TypeErrorPass = "nonewvars"
 	NoResultValues TypeErrorPass = "noresultvalues"
 	UndeclaredName TypeErrorPass = "undeclaredname"
 )
diff --git a/internal/lsp/analysis/nonewvars/nonewvars.go b/internal/lsp/analysis/nonewvars/nonewvars.go
new file mode 100644
index 0000000..31dcd25
--- /dev/null
+++ b/internal/lsp/analysis/nonewvars/nonewvars.go
@@ -0,0 +1,91 @@
+// Copyright 2020 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 nonewvars defines an Analyzer that applies suggested fixes
+// to errors of the type "no new variables on left side of :=".
+package nonewvars
+
+import (
+	"bytes"
+	"go/ast"
+	"go/format"
+	"go/token"
+
+	"golang.org/x/tools/go/analysis"
+	"golang.org/x/tools/go/analysis/passes/inspect"
+	"golang.org/x/tools/go/ast/inspector"
+	"golang.org/x/tools/internal/analysisinternal"
+)
+
+const Doc = `suggested fixes for "no new vars on left side of :="
+
+This checker provides suggested fixes for type errors of the
+type "no new vars on left side of :=". For example:
+	z := 1
+	z := 2
+will turn into
+	z := 1
+	z = 2
+`
+
+var Analyzer = &analysis.Analyzer{
+	Name:             string(analysisinternal.NoNewVars),
+	Doc:              Doc,
+	Requires:         []*analysis.Analyzer{inspect.Analyzer},
+	Run:              run,
+	RunDespiteErrors: true,
+}
+
+const noNewVarsMsg = "no new variables on left side of :="
+
+func run(pass *analysis.Pass) (interface{}, error) {
+	inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
+	errors := analysisinternal.GetTypeErrors(pass)
+
+	nodeFilter := []ast.Node{(*ast.AssignStmt)(nil)}
+	inspect.Preorder(nodeFilter, func(n ast.Node) {
+		assignStmt, _ := n.(*ast.AssignStmt)
+		// We only care about ":=".
+		if assignStmt.Tok != token.DEFINE {
+			return
+		}
+
+		var file *ast.File
+		for _, f := range pass.Files {
+			if f.Pos() <= assignStmt.Pos() && assignStmt.Pos() < f.End() {
+				file = f
+				break
+			}
+		}
+		if file == nil {
+			return
+		}
+
+		for _, err := range errors {
+			if err.Msg != noNewVarsMsg {
+				continue
+			}
+			if assignStmt.Pos() > err.Pos || err.Pos >= assignStmt.End() {
+				continue
+			}
+			var buf bytes.Buffer
+			if err := format.Node(&buf, pass.Fset, file); err != nil {
+				continue
+			}
+			pass.Report(analysis.Diagnostic{
+				Pos:     err.Pos,
+				End:     analysisinternal.TypeErrorEndPos(pass.Fset, buf.Bytes(), err.Pos),
+				Message: err.Msg,
+				SuggestedFixes: []analysis.SuggestedFix{{
+					Message: "Change ':=' to '='",
+					TextEdits: []analysis.TextEdit{{
+						Pos: err.Pos,
+						End: err.Pos + 1,
+					}},
+				}},
+			})
+		}
+	})
+	return nil, nil
+}
diff --git a/internal/lsp/analysis/nonewvars/nonewvars_test.go b/internal/lsp/analysis/nonewvars/nonewvars_test.go
new file mode 100644
index 0000000..3983bc5
--- /dev/null
+++ b/internal/lsp/analysis/nonewvars/nonewvars_test.go
@@ -0,0 +1,17 @@
+// Copyright 2020 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 nonewvars_test
+
+import (
+	"testing"
+
+	"golang.org/x/tools/go/analysis/analysistest"
+	"golang.org/x/tools/internal/lsp/analysis/nonewvars"
+)
+
+func Test(t *testing.T) {
+	testdata := analysistest.TestData()
+	analysistest.RunWithSuggestedFixes(t, testdata, nonewvars.Analyzer, "a")
+}
diff --git a/internal/lsp/analysis/nonewvars/testdata/src/a/a.go b/internal/lsp/analysis/nonewvars/testdata/src/a/a.go
new file mode 100644
index 0000000..97d8fcd
--- /dev/null
+++ b/internal/lsp/analysis/nonewvars/testdata/src/a/a.go
@@ -0,0 +1,16 @@
+// Copyright 2020 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 nonewvars
+
+import "log"
+
+func x() {
+	z := 1
+	z := 2 // want "no new variables on left side of :="
+
+	_, z := 3, 100 // want "no new variables on left side of :="
+
+	log.Println(z)
+}
diff --git a/internal/lsp/analysis/nonewvars/testdata/src/a/a.go.golden b/internal/lsp/analysis/nonewvars/testdata/src/a/a.go.golden
new file mode 100644
index 0000000..17197e5
--- /dev/null
+++ b/internal/lsp/analysis/nonewvars/testdata/src/a/a.go.golden
@@ -0,0 +1,16 @@
+// Copyright 2020 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 nonewvars
+
+import "log"
+
+func x() {
+	z := 1
+	z = 2 // want "no new variables on left side of :="
+
+	_, z = 3, 100 // want "no new variables on left side of :="
+
+	log.Println(z)
+}
diff --git a/internal/lsp/source/options.go b/internal/lsp/source/options.go
index 57288e1..568a20c 100644
--- a/internal/lsp/source/options.go
+++ b/internal/lsp/source/options.go
@@ -36,6 +36,7 @@
 	"golang.org/x/tools/go/analysis/passes/unreachable"
 	"golang.org/x/tools/go/analysis/passes/unsafeptr"
 	"golang.org/x/tools/go/analysis/passes/unusedresult"
+	"golang.org/x/tools/internal/lsp/analysis/nonewvars"
 	"golang.org/x/tools/internal/lsp/analysis/noresultvalues"
 	"golang.org/x/tools/internal/lsp/analysis/simplifycompositelit"
 	"golang.org/x/tools/internal/lsp/analysis/simplifyrange"
@@ -488,6 +489,7 @@
 
 func typeErrorAnalyzers() map[string]Analyzer {
 	return map[string]Analyzer{
+		nonewvars.Analyzer.Name:      {Analyzer: nonewvars.Analyzer, Enabled: true},
 		noresultvalues.Analyzer.Name: {Analyzer: noresultvalues.Analyzer, Enabled: true},
 		undeclaredname.Analyzer.Name: {Analyzer: undeclaredname.Analyzer, Enabled: true},
 	}