blob: aac9ab769092332c97ee7c8c13294029286d0d1b [file] [log] [blame]
Tim Windelschmidt1f4590b2025-07-29 23:05:36 +02001// Copyright 2016 The Cockroach Authors.
2// SPDX-License-Identifier: Apache-2.0
3
4// Package hash defines an Analyzer that detects correct use of hash.Hash.
5package hash
6
7import (
8 "go/ast"
9 "go/types"
10
11 "golang.org/x/tools/go/analysis"
12 "golang.org/x/tools/go/analysis/passes/inspect"
13)
14
15// Doc documents this pass.
16const Doc = `check for correct use of hash.Hash`
17
18// Analyzer defines this pass.
19var Analyzer = &analysis.Analyzer{
20 Name: "hash",
21 Doc: Doc,
22 Requires: []*analysis.Analyzer{inspect.Analyzer},
23 Run: run,
24}
25
26// hashChecker assures that the hash.Hash interface is not misused. A common
27// mistake is to assume that the Sum function returns the hash of its input,
28// like so:
29//
30// hashedBytes := sha256.New().Sum(inputBytes)
31//
32// In fact, the parameter to Sum is not the bytes to be hashed, but a slice that
33// will be used as output in case the caller wants to avoid an allocation. In
34// the example above, hashedBytes is not the SHA-256 hash of inputBytes, but
35// the concatenation of inputBytes with the hash of the empty string.
36//
37// Correct uses of the hash.Hash interface are as follows:
38//
39// h := sha256.New()
40// h.Write(inputBytes)
41// hashedBytes := h.Sum(nil)
42//
43// h := sha256.New()
44// h.Write(inputBytes)
45// var hashedBytes [sha256.Size]byte
46// h.Sum(hashedBytes[:0])
47//
48// To differentiate between correct and incorrect usages, hashChecker applies a
49// simple heuristic: it flags calls to Sum where a) the parameter is non-nil and
50// b) the return value is used.
51//
52// The hash.Hash interface may be remedied in Go 2. See golang/go#21070.
53func run(pass *analysis.Pass) (interface{}, error) {
54 selectorIsHash := func(s *ast.SelectorExpr) bool {
55 tv, ok := pass.TypesInfo.Types[s.X]
56 if !ok {
57 return false
58 }
59 named, ok := tv.Type.(*types.Named)
60 if !ok {
61 return false
62 }
63 if named.Obj().Type().String() != "hash.Hash" {
64 return false
65 }
66 return true
67 }
68
69 stack := make([]ast.Node, 0, 32)
70 forAllFiles(pass.Files, func(n ast.Node) bool {
71 if n == nil {
72 stack = stack[:len(stack)-1] // pop
73 return true
74 }
75 stack = append(stack, n) // push
76
77 // Find a call to hash.Hash.Sum.
78 selExpr, ok := n.(*ast.SelectorExpr)
79 if !ok {
80 return true
81 }
82 if selExpr.Sel.Name != "Sum" {
83 return true
84 }
85 if !selectorIsHash(selExpr) {
86 return true
87 }
88 callExpr, ok := stack[len(stack)-2].(*ast.CallExpr)
89 if !ok {
90 return true
91 }
92 if len(callExpr.Args) != 1 {
93 return true
94 }
95 // We have a valid call to hash.Hash.Sum.
96
97 // Is the argument nil?
98 var nilArg bool
99 if id, ok := callExpr.Args[0].(*ast.Ident); ok && id.Name == "nil" {
100 nilArg = true
101 }
102
103 // Is the return value unused?
104 var retUnused bool
105 Switch:
106 switch t := stack[len(stack)-3].(type) {
107 case *ast.AssignStmt:
108 for i := range t.Rhs {
109 if t.Rhs[i] == stack[len(stack)-2] {
110 if id, ok := t.Lhs[i].(*ast.Ident); ok && id.Name == "_" {
111 // Assigning to the blank identifier does not count as using the
112 // return value.
113 retUnused = true
114 }
115 break Switch
116 }
117 }
118 panic("unreachable")
119 case *ast.ExprStmt:
120 // An expression statement means the return value is unused.
121 retUnused = true
122 default:
123 }
124
125 if !nilArg && !retUnused {
126 pass.Reportf(callExpr.Pos(), "probable misuse of hash.Hash.Sum: "+
127 "provide parameter or use return value, but not both")
128 }
129 return true
130 })
131
132 return nil, nil
133}
134
135func forAllFiles(files []*ast.File, fn func(node ast.Node) bool) {
136 for _, f := range files {
137 ast.Inspect(f, fn)
138 }
139}