1// Copyright 2020 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5// Package simplifyrange defines an Analyzer that simplifies range statements.
6// https://golang.org/cmd/gofmt/#hdr-The_simplify_command
7// https://github.com/golang/go/blob/master/src/cmd/gofmt/simplify.go
8package simplifyrange
9
10import (
11	"bytes"
12	"go/ast"
13	"go/printer"
14	"go/token"
15
16	"golang.org/x/tools/go/analysis"
17	"golang.org/x/tools/go/analysis/passes/inspect"
18	"golang.org/x/tools/go/ast/inspector"
19)
20
21const Doc = `check for range statement simplifications
22
23A range of the form:
24	for x, _ = range v {...}
25will be simplified to:
26	for x = range v {...}
27
28A range of the form:
29	for _ = range v {...}
30will be simplified to:
31	for range v {...}
32
33This is one of the simplifications that "gofmt -s" applies.`
34
35var Analyzer = &analysis.Analyzer{
36	Name:     "simplifyrange",
37	Doc:      Doc,
38	Requires: []*analysis.Analyzer{inspect.Analyzer},
39	Run:      run,
40}
41
42func run(pass *analysis.Pass) (interface{}, error) {
43	inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
44	nodeFilter := []ast.Node{
45		(*ast.RangeStmt)(nil),
46	}
47	inspect.Preorder(nodeFilter, func(n ast.Node) {
48		var copy *ast.RangeStmt
49		if stmt, ok := n.(*ast.RangeStmt); ok {
50			x := *stmt
51			copy = &x
52		}
53		if copy == nil {
54			return
55		}
56		end := newlineIndex(pass.Fset, copy)
57
58		// Range statements of the form: for i, _ := range x {}
59		var old ast.Expr
60		if isBlank(copy.Value) {
61			old = copy.Value
62			copy.Value = nil
63		}
64		// Range statements of the form: for _ := range x {}
65		if isBlank(copy.Key) && copy.Value == nil {
66			old = copy.Key
67			copy.Key = nil
68		}
69		// Return early if neither if condition is met.
70		if old == nil {
71			return
72		}
73		pass.Report(analysis.Diagnostic{
74			Pos:            old.Pos(),
75			End:            old.End(),
76			Message:        "simplify range expression",
77			SuggestedFixes: suggestedFixes(pass.Fset, copy, end),
78		})
79	})
80	return nil, nil
81}
82
83func suggestedFixes(fset *token.FileSet, rng *ast.RangeStmt, end token.Pos) []analysis.SuggestedFix {
84	var b bytes.Buffer
85	printer.Fprint(&b, fset, rng)
86	stmt := b.Bytes()
87	index := bytes.Index(stmt, []byte("\n"))
88	// If there is a new line character, then don't replace the body.
89	if index != -1 {
90		stmt = stmt[:index]
91	}
92	return []analysis.SuggestedFix{{
93		Message: "Remove empty value",
94		TextEdits: []analysis.TextEdit{{
95			Pos:     rng.Pos(),
96			End:     end,
97			NewText: stmt[:index],
98		}},
99	}}
100}
101
102func newlineIndex(fset *token.FileSet, rng *ast.RangeStmt) token.Pos {
103	var b bytes.Buffer
104	printer.Fprint(&b, fset, rng)
105	contents := b.Bytes()
106	index := bytes.Index(contents, []byte("\n"))
107	if index == -1 {
108		return rng.End()
109	}
110	return rng.Pos() + token.Pos(index)
111}
112
113func isBlank(x ast.Expr) bool {
114	ident, ok := x.(*ast.Ident)
115	return ok && ident.Name == "_"
116}
117