1package checkers
2
3import (
4	"go/ast"
5	"go/types"
6	"strings"
7
8	"github.com/go-critic/go-critic/checkers/internal/astwalk"
9	"github.com/go-critic/go-critic/checkers/internal/lintutil"
10	"github.com/go-critic/go-critic/framework/linter"
11	"github.com/go-toolsmith/astcast"
12	"github.com/go-toolsmith/astp"
13	"github.com/go-toolsmith/typep"
14)
15
16func init() {
17	var info linter.CheckerInfo
18	info.Name = "mapKey"
19	info.Tags = []string{"diagnostic"}
20	info.Summary = "Detects suspicious map literal keys"
21	info.Before = `
22_ = map[string]int{
23	"foo": 1,
24	"bar ": 2,
25}`
26	info.After = `
27_ = map[string]int{
28	"foo": 1,
29	"bar": 2,
30}`
31
32	collection.AddChecker(&info, func(ctx *linter.CheckerContext) (linter.FileWalker, error) {
33		return astwalk.WalkerForExpr(&mapKeyChecker{ctx: ctx}), nil
34	})
35}
36
37type mapKeyChecker struct {
38	astwalk.WalkHandler
39	ctx *linter.CheckerContext
40
41	astSet lintutil.AstSet
42}
43
44func (c *mapKeyChecker) VisitExpr(expr ast.Expr) {
45	lit := astcast.ToCompositeLit(expr)
46	if len(lit.Elts) < 2 {
47		return
48	}
49
50	typ, ok := c.ctx.TypeOf(lit).Underlying().(*types.Map)
51	if !ok {
52		return
53	}
54	if !typep.HasStringKind(typ.Key().Underlying()) {
55		return
56	}
57
58	c.checkWhitespace(lit)
59	c.checkDuplicates(lit)
60}
61
62func (c *mapKeyChecker) checkDuplicates(lit *ast.CompositeLit) {
63	c.astSet.Clear()
64
65	for _, elt := range lit.Elts {
66		kv := astcast.ToKeyValueExpr(elt)
67		if astp.IsBasicLit(kv.Key) {
68			// Basic lits are handled by the compiler.
69			continue
70		}
71		if !typep.SideEffectFree(c.ctx.TypesInfo, kv.Key) {
72			continue
73		}
74		if !c.astSet.Insert(kv.Key) {
75			c.warnDupKey(kv.Key)
76		}
77	}
78}
79
80func (c *mapKeyChecker) checkWhitespace(lit *ast.CompositeLit) {
81	var whitespaceKey ast.Node
82	for _, elt := range lit.Elts {
83		key := astcast.ToBasicLit(astcast.ToKeyValueExpr(elt).Key)
84		if len(key.Value) < len(`" "`) {
85			continue
86		}
87		// s is unquoted string literal value.
88		s := key.Value[len(`"`) : len(key.Value)-len(`"`)]
89		if !strings.Contains(s, " ") {
90			continue
91		}
92		if whitespaceKey != nil {
93			// Already seen something with a whitespace.
94			// More than one entry => not suspicious.
95			return
96		}
97		if s == " " {
98			// If space is used as a key, maybe this map
99			// has something to do with spaces. Give up.
100			return
101		}
102		// Check if it has exactly 1 space prefix or suffix.
103		bad := strings.HasPrefix(s, " ") && !strings.HasPrefix(s, "  ") ||
104			strings.HasSuffix(s, " ") && !strings.HasSuffix(s, "  ")
105		if !bad {
106			// These spaces can be a padding,
107			// or a legitimate part of a key. Give up.
108			return
109		}
110		whitespaceKey = key
111	}
112
113	if whitespaceKey != nil {
114		c.warnWhitespace(whitespaceKey)
115	}
116}
117
118func (c *mapKeyChecker) warnWhitespace(key ast.Node) {
119	c.ctx.Warn(key, "suspucious whitespace in %s key", key)
120}
121
122func (c *mapKeyChecker) warnDupKey(key ast.Node) {
123	c.ctx.Warn(key, "suspicious duplicate %s key", key)
124}
125