Skip to content

Commit 55b751a

Browse files
adonovangopherbot
authored andcommitted
gopls/internal/golang: InlayHint: reveal ignored errors
This new inlay hint adds "// ignore error" after a call statement that discards its error result. + doc, test, relnote Fixes golang/go#73930 Change-Id: I051b68c80b24eeeecf393928d8942114c519f9fa Reviewed-on: https://go-review.googlesource.com/c/tools/+/683835 Reviewed-by: Robert Findley <[email protected]> Auto-Submit: Alan Donovan <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent a0ae5fa commit 55b751a

File tree

7 files changed

+202
-13
lines changed

7 files changed

+202
-13
lines changed

gopls/doc/inlayHints.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,25 @@ from `gopls`.
6666

6767
**Disabled by default. Enable it by setting `"hints": {"functionTypeParameters": true}`.**
6868

69+
## **ignoredError**
70+
71+
`"ignoredError"` inlay hints for implicitly discarded errors:
72+
```go
73+
f.Close() // ignore error
74+
```
75+
This check inserts an `// ignore error` hint following any
76+
statement that is a function call whose error result is
77+
implicitly ignored.
78+
79+
To suppress the hint, write an actual comment containing
80+
"ignore error" following the call statement, or explictly
81+
assign the result to a blank variable. A handful of common
82+
functions such as `fmt.Println` are excluded from the
83+
check.
84+
85+
86+
**Disabled by default. Enable it by setting `"hints": {"ignoredError": true}`.**
87+
6988
## **parameterNames**
7089

7190
`"parameterNames"` controls inlay hints for parameter names:

gopls/doc/release/v0.20.0.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,28 @@ $description
1212

1313
# Analysis features
1414

15+
## `ignoredError` inlay hint
16+
17+
The new `ignoredError` inlay hint helps catch mistakenly discarded
18+
errors. It inserts an `// ignore error` hint following any statement
19+
that is a function call whose error result is implicitly ignored. For
20+
example, this code:
21+
22+
```go
23+
f.Close()
24+
```
25+
will appear as:
26+
```go
27+
f.Close() // ignore error
28+
```
29+
30+
To suppress the hint, write an actual comment containing `ignore
31+
error` following the call statement, or explictly assign the result
32+
to a blank `_` variable. A handful of common functions such as
33+
`fmt.Println` are excluded from the check.
34+
35+
Enable it using this configuration: `{"hints": {"ignoredError": true}}`.
36+
1537
## `unusedfunc` reports unused `type`, `var`, and `const` declarations too
1638

1739
<!-- golang/go#40862 -->

gopls/internal/doc/api.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1878,6 +1878,12 @@
18781878
"Default": "false",
18791879
"Status": ""
18801880
},
1881+
{
1882+
"Name": "\"ignoredError\"",
1883+
"Doc": "`\"ignoredError\"` inlay hints for implicitly discarded errors:\n```go\n\tf.Close() // ignore error\n```\nThis check inserts an `// ignore error` hint following any\nstatement that is a function call whose error result is\nimplicitly ignored.\n\nTo suppress the hint, write an actual comment containing\n\"ignore error\" following the call statement, or explictly\nassign the result to a blank variable. A handful of common\nfunctions such as `fmt.Println` are excluded from the\ncheck.\n",
1884+
"Default": "false",
1885+
"Status": ""
1886+
},
18811887
{
18821888
"Name": "\"parameterNames\"",
18831889
"Doc": "`\"parameterNames\"` controls inlay hints for parameter names:\n```go\n\tparseInt(/* str: */ \"123\", /* radix: */ 8)\n```\n",
@@ -3452,6 +3458,12 @@
34523458
"Default": false,
34533459
"Status": ""
34543460
},
3461+
{
3462+
"Name": "ignoredError",
3463+
"Doc": "`\"ignoredError\"` inlay hints for implicitly discarded errors:\n```go\n\tf.Close() // ignore error\n```\nThis check inserts an `// ignore error` hint following any\nstatement that is a function call whose error result is\nimplicitly ignored.\n\nTo suppress the hint, write an actual comment containing\n\"ignore error\" following the call statement, or explictly\nassign the result to a blank variable. A handful of common\nfunctions such as `fmt.Println` are excluded from the\ncheck.\n",
3464+
"Default": false,
3465+
"Status": ""
3466+
},
34553467
{
34563468
"Name": "parameterNames",
34573469
"Doc": "`\"parameterNames\"` controls inlay hints for parameter names:\n```go\n\tparseInt(/* str: */ \"123\", /* radix: */ 8)\n```\n",

gopls/internal/golang/inlay_hint.go

Lines changed: 69 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,24 @@
55
package golang
66

77
import (
8+
"cmp"
89
"context"
910
"fmt"
1011
"go/ast"
1112
"go/constant"
1213
"go/token"
1314
"go/types"
15+
"slices"
1416
"strings"
1517

1618
"golang.org/x/tools/go/ast/inspector"
19+
"golang.org/x/tools/go/types/typeutil"
1720
"golang.org/x/tools/gopls/internal/cache"
1821
"golang.org/x/tools/gopls/internal/cache/parsego"
1922
"golang.org/x/tools/gopls/internal/file"
2023
"golang.org/x/tools/gopls/internal/protocol"
2124
"golang.org/x/tools/gopls/internal/settings"
25+
"golang.org/x/tools/internal/analysisinternal"
2226
"golang.org/x/tools/internal/event"
2327
"golang.org/x/tools/internal/typeparams"
2428
"golang.org/x/tools/internal/typesinternal"
@@ -84,6 +88,7 @@ var allInlayHints = map[settings.InlayHint]inlayHintFunc{
8488
settings.CompositeLiteralTypes: compositeLiteralTypes,
8589
settings.CompositeLiteralFieldNames: compositeLiteralFields,
8690
settings.FunctionTypeParameters: funcTypeParams,
91+
settings.IgnoredError: ignoredError,
8792
}
8893

8994
func parameterNames(info *types.Info, pgf *parsego.File, qual types.Qualifier, cur inspector.Cursor, add func(protocol.InlayHint)) {
@@ -126,14 +131,69 @@ func parameterNames(info *types.Info, pgf *parsego.File, qual types.Qualifier, c
126131
}
127132
add(protocol.InlayHint{
128133
Position: start,
129-
Label: buildLabel(label + ":"),
134+
Label: labelPart(label + ":"),
130135
Kind: protocol.Parameter,
131136
PaddingRight: true,
132137
})
133138
}
134139
}
135140
}
136141

142+
func ignoredError(info *types.Info, pgf *parsego.File, qual types.Qualifier, cur inspector.Cursor, add func(protocol.InlayHint)) {
143+
outer:
144+
for curCall := range cur.Preorder((*ast.ExprStmt)(nil)) {
145+
stmt := curCall.Node().(*ast.ExprStmt)
146+
call, ok := stmt.X.(*ast.CallExpr)
147+
if !ok {
148+
continue // not a call stmt
149+
}
150+
151+
// Check that type of result (or last component) is error.
152+
tv, ok := info.Types[call]
153+
if !ok {
154+
continue // no type info
155+
}
156+
t := tv.Type
157+
if res, ok := t.(*types.Tuple); ok && res.Len() > 1 {
158+
t = res.At(res.Len() - 1).Type()
159+
}
160+
if !types.Identical(t, errorType) {
161+
continue
162+
}
163+
164+
// Suppress some common false positives.
165+
obj := typeutil.Callee(info, call)
166+
if analysisinternal.IsFunctionNamed(obj, "fmt", "Print", "Printf", "Println", "Fprint", "Fprintf", "Fprintln") ||
167+
analysisinternal.IsMethodNamed(obj, "bytes", "Buffer", "Write", "WriteByte", "WriteRune", "WriteString") ||
168+
analysisinternal.IsMethodNamed(obj, "strings", "Builder", "Write", "WriteByte", "WriteRune", "WriteString") {
169+
continue
170+
}
171+
172+
// Suppress if closely following comment contains "// ignore error".
173+
comments := pgf.File.Comments
174+
compare := func(cg *ast.CommentGroup, pos token.Pos) int {
175+
return cmp.Compare(cg.Pos(), pos)
176+
}
177+
i, _ := slices.BinarySearchFunc(comments, stmt.End(), compare)
178+
if i >= 0 && i < len(comments) {
179+
cg := comments[i]
180+
if cg.Pos() < stmt.End()+3 && strings.Contains(cg.Text(), "ignore error") {
181+
continue outer // suppress
182+
}
183+
}
184+
185+
// Provide a hint.
186+
pos, err := pgf.PosPosition(stmt.End())
187+
if err != nil {
188+
continue
189+
}
190+
add(protocol.InlayHint{
191+
Position: pos,
192+
Label: labelPart(" // ignore error"),
193+
})
194+
}
195+
}
196+
137197
func funcTypeParams(info *types.Info, pgf *parsego.File, qual types.Qualifier, cur inspector.Cursor, add func(protocol.InlayHint)) {
138198
for curCall := range cur.Preorder((*ast.CallExpr)(nil)) {
139199
call := curCall.Node().(*ast.CallExpr)
@@ -158,7 +218,7 @@ func funcTypeParams(info *types.Info, pgf *parsego.File, qual types.Qualifier, c
158218
}
159219
add(protocol.InlayHint{
160220
Position: start,
161-
Label: buildLabel("[" + strings.Join(args, ", ") + "]"),
221+
Label: labelPart("[" + strings.Join(args, ", ") + "]"),
162222
Kind: protocol.Type,
163223
})
164224
}
@@ -215,7 +275,7 @@ func variableType(info *types.Info, pgf *parsego.File, qual types.Qualifier, e a
215275
}
216276
add(protocol.InlayHint{
217277
Position: end,
218-
Label: buildLabel(types.TypeString(typ, qual)),
278+
Label: labelPart(types.TypeString(typ, qual)),
219279
Kind: protocol.Type,
220280
PaddingLeft: true,
221281
})
@@ -265,7 +325,7 @@ func constantValues(info *types.Info, pgf *parsego.File, qual types.Qualifier, c
265325
}
266326
add(protocol.InlayHint{
267327
Position: end,
268-
Label: buildLabel("= " + strings.Join(values, ", ")),
328+
Label: labelPart("= " + strings.Join(values, ", ")),
269329
PaddingLeft: true,
270330
})
271331
}
@@ -301,7 +361,7 @@ func compositeLiteralFields(info *types.Info, pgf *parsego.File, qual types.Qual
301361
}
302362
hints = append(hints, protocol.InlayHint{
303363
Position: start,
304-
Label: buildLabel(strct.Field(i).Name() + ":"),
364+
Label: labelPart(strct.Field(i).Name() + ":"),
305365
Kind: protocol.Parameter,
306366
PaddingRight: true,
307367
})
@@ -342,19 +402,16 @@ func compositeLiteralTypes(info *types.Info, pgf *parsego.File, qual types.Quali
342402
}
343403
add(protocol.InlayHint{
344404
Position: start,
345-
Label: buildLabel(fmt.Sprintf("%s%s", prefix, types.TypeString(typ, qual))),
405+
Label: labelPart(fmt.Sprintf("%s%s", prefix, types.TypeString(typ, qual))),
346406
Kind: protocol.Type,
347407
})
348408
}
349409
}
350410

351-
func buildLabel(s string) []protocol.InlayHintLabelPart {
411+
func labelPart(s string) []protocol.InlayHintLabelPart {
352412
const maxLabelLength = 28
353-
label := protocol.InlayHintLabelPart{
354-
Value: s,
355-
}
356413
if len(s) > maxLabelLength+len("...") {
357-
label.Value = s[:maxLabelLength] + "..."
414+
s = s[:maxLabelLength] + "..."
358415
}
359-
return []protocol.InlayHintLabelPart{label}
416+
return []protocol.InlayHintLabelPart{{Value: s}}
360417
}

gopls/internal/settings/default.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,9 @@ func DefaultOptions(overrides ...func(*Options)) *Options {
103103
DiagnosticsTrigger: DiagnosticsOnEdit,
104104
AnalysisProgressReporting: true,
105105
},
106-
InlayHintOptions: InlayHintOptions{},
106+
InlayHintOptions: InlayHintOptions{
107+
Hints: map[InlayHint]bool{},
108+
},
107109
DocumentationOptions: DocumentationOptions{
108110
HoverKind: FullDocumentation,
109111
LinkTarget: "pkg.go.dev",
@@ -150,5 +152,6 @@ func DefaultOptions(overrides ...func(*Options)) *Options {
150152
override(options)
151153
}
152154
}
155+
153156
return options
154157
}

gopls/internal/settings/settings.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,21 @@ const (
598598
// myFoo/*[int, string]*/(1, "hello")
599599
// ```
600600
FunctionTypeParameters InlayHint = "functionTypeParameters"
601+
602+
// IgnoredError inlay hints for implicitly discarded errors:
603+
// ```go
604+
// f.Close() // ignore error
605+
// ```
606+
// This check inserts an `// ignore error` hint following any
607+
// statement that is a function call whose error result is
608+
// implicitly ignored.
609+
//
610+
// To suppress the hint, write an actual comment containing
611+
// "ignore error" following the call statement, or explictly
612+
// assign the result to a blank variable. A handful of common
613+
// functions such as `fmt.Println` are excluded from the
614+
// check.
615+
IgnoredError InlayHint = "ignoredError"
601616
)
602617

603618
type NavigationOptions struct {
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
Test of "ignore error" inlay hint (#73930).
2+
3+
- f.Close() generates a hint, except when followed by
4+
an "// ignore error" comment, or in a "_ = f.Close()" stmt.
5+
- fmt.Println() is exempted.
6+
7+
-- settings.json --
8+
{"hints": {"ignoredError": true}}
9+
10+
-- p/p.go --
11+
package p //@inlayhints(out)
12+
13+
import ( "os"; "fmt" )
14+
15+
func _(f *os.File) {
16+
f.WriteString("hello")
17+
f.Close()
18+
}
19+
20+
func _(f *os.File) {
21+
f.Close() // irrelevant comment
22+
}
23+
24+
func _(f *os.File) {
25+
f.Close() // ignore error
26+
}
27+
28+
func _(f *os.File) {
29+
_ = f.Close()
30+
}
31+
32+
func _() {
33+
fmt.Println()
34+
}
35+
36+
-- @out --
37+
package p //@inlayhints(out)
38+
39+
import ( "os"; "fmt" )
40+
41+
func _(f *os.File) {
42+
f.WriteString("hello")< // ignore error>
43+
f.Close()< // ignore error>
44+
}
45+
46+
func _(f *os.File) {
47+
f.Close()< // ignore error> // irrelevant comment
48+
}
49+
50+
func _(f *os.File) {
51+
f.Close() // ignore error
52+
}
53+
54+
func _(f *os.File) {
55+
_ = f.Close()
56+
}
57+
58+
func _() {
59+
fmt.Println()
60+
}
61+

0 commit comments

Comments
 (0)