Skip to content

Commit 95acb88

Browse files
Dirk007Dirk Faust
and
Dirk Faust
authored
Add unchecked-type-assertion (#889)
Co-authored-by: Dirk Faust <[email protected]>
1 parent 3da2646 commit 95acb88

8 files changed

+360
-3
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,7 @@ List of all available rules. The rules ported from `golint` are left unchanged a
464464
| [`context-keys-type`](./RULES_DESCRIPTIONS.md#context-key-types) | n/a | Disallows the usage of basic types in `context.WithValue`. | yes | yes |
465465
| [`time-equal`](./RULES_DESCRIPTIONS.md#time-equal) | n/a | Suggests to use `time.Time.Equal` instead of `==` and `!=` for equality check time. | no | yes |
466466
| [`time-naming`](./RULES_DESCRIPTIONS.md#time-naming) | n/a | Conventions around the naming of time variables. | yes | yes |
467+
| [`unchecked-type-assertions`](./RULES_DESCRIPTIONS.md#unchecked-type-assertions) | n/a | Disallows type assertions without checking the result. | no | yes |
467468
| [`var-declaration`](./RULES_DESCRIPTIONS.md#var-declaration) | n/a | Reduces redundancies around variable declaration. | yes | yes |
468469
| [`unexported-return`](./RULES_DESCRIPTIONS.md#unexported-return) | n/a | Warns when a public return is from unexported type. | yes | yes |
469470
| [`errorf`](./RULES_DESCRIPTIONS.md#errorf) | n/a | Should replace `errors.New(fmt.Sprintf())` with `fmt.Errorf()` | yes | yes |

RULES_DESCRIPTIONS.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ List of all available rules.
6262
- [string-format](#string-format)
6363
- [superfluous-else](#superfluous-else)
6464
- [time-equal](#time-equal)
65+
- [unchecked-type-assertion](#unchecked-type-assertion)
6566
- [time-naming](#time-naming)
6667
- [var-naming](#var-naming)
6768
- [var-declaration](#var-declaration)
@@ -170,8 +171,8 @@ Example:
170171
[rule.cognitive-complexity]
171172
arguments =[7]
172173
```
173-
174174
## comment-spacings
175+
175176
_Description_: Spots comments of the form:
176177
```go
177178
//This is a malformed comment: no space between // and the start of the sentence
@@ -683,6 +684,26 @@ _Description_: Using unit-specific suffix like "Secs", "Mins", ... when naming v
683684

684685
_Configuration_: N/A
685686

687+
## unchecked-type-assertion
688+
689+
_Description_: This rule checks whether a type assertion result is checked (the `ok` value), preventing unexpected `panic`s.
690+
691+
_Configuration_: list of key-value-pair-map (`[]map[string]any`).
692+
693+
- `acceptIgnoredAssertionResult` : (bool) default `false`, set it to `true` to accept ignored type assertion results like this:
694+
695+
```go
696+
foo, _ := bar(.*Baz).
697+
// ^
698+
```
699+
700+
Example:
701+
702+
```yaml
703+
[rule.unchecked-type-assertion]
704+
arguments = [{acceptIgnoredAssertionResult=true}]
705+
```
706+
686707
## var-naming
687708

688709
_Description_: This rule warns when [initialism](https://github.com/golang/go/wiki/CodeReviewComments#initialisms), [variable](https://github.com/golang/go/wiki/CodeReviewComments#variable-names) or [package](https://github.com/golang/go/wiki/CodeReviewComments#package-names) naming conventions are not followed.

config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ var allRules = append([]lint.Rule{
8080
&rule.FunctionLength{},
8181
&rule.NestedStructs{},
8282
&rule.UselessBreak{},
83+
&rule.UncheckedTypeAssertionRule{},
8384
&rule.TimeEqualRule{},
8485
&rule.BannedCharsRule{},
8586
&rule.OptimizeOperandsOrderRule{},

go.sum

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
22
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
3-
github.com/chavacava/garif v0.0.0-20230608123814-4bd63c2919ab h1:5JxePczlyGAtj6R1MUEFZ/UFud6FfsOejq7xLC2ZIb0=
4-
github.com/chavacava/garif v0.0.0-20230608123814-4bd63c2919ab/go.mod h1:XMyYCkEL58DF0oyW4qDjjnPWONs2HBqYKI+UIPD+Gww=
53
github.com/chavacava/garif v0.1.0 h1:2JHa3hbYf5D9dsgseMKAmc/MZ109otzgNFk5s87H9Pc=
64
github.com/chavacava/garif v0.1.0/go.mod h1:XMyYCkEL58DF0oyW4qDjjnPWONs2HBqYKI+UIPD+Gww=
75
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

rule/unchecked-type-assertion.go

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
package rule
2+
3+
import (
4+
"fmt"
5+
"go/ast"
6+
"sync"
7+
8+
"github.com/mgechev/revive/lint"
9+
)
10+
11+
const (
12+
ruleUTAMessagePanic = "type assertion will panic if not matched"
13+
ruleUTAMessageIgnored = "type assertion result ignored"
14+
)
15+
16+
// UncheckedTypeAssertionRule lints missing or ignored `ok`-value in danymic type casts.
17+
type UncheckedTypeAssertionRule struct {
18+
sync.Mutex
19+
acceptIgnoredAssertionResult bool
20+
}
21+
22+
func (u *UncheckedTypeAssertionRule) configure(arguments lint.Arguments) {
23+
u.Lock()
24+
defer u.Unlock()
25+
26+
if len(arguments) == 0 {
27+
return
28+
}
29+
30+
args, ok := arguments[0].(map[string]any)
31+
if !ok {
32+
panic("Unable to get arguments. Expected object of key-value-pairs.")
33+
}
34+
35+
for k, v := range args {
36+
switch k {
37+
case "acceptIgnoredAssertionResult":
38+
u.acceptIgnoredAssertionResult, ok = v.(bool)
39+
if !ok {
40+
panic(fmt.Sprintf("Unable to parse argument '%s'. Expected boolean.", k))
41+
}
42+
default:
43+
panic(fmt.Sprintf("Unknown argument: %s", k))
44+
}
45+
}
46+
}
47+
48+
// Apply applies the rule to given file.
49+
func (u *UncheckedTypeAssertionRule) Apply(file *lint.File, args lint.Arguments) []lint.Failure {
50+
u.configure(args)
51+
52+
var failures []lint.Failure
53+
54+
walker := &lintUnchekedTypeAssertion{
55+
pkg: file.Pkg,
56+
onFailure: func(failure lint.Failure) {
57+
failures = append(failures, failure)
58+
},
59+
acceptIgnoredTypeAssertionResult: u.acceptIgnoredAssertionResult,
60+
}
61+
62+
file.Pkg.TypeCheck()
63+
ast.Walk(walker, file.AST)
64+
65+
return failures
66+
}
67+
68+
// Name returns the rule name.
69+
func (*UncheckedTypeAssertionRule) Name() string {
70+
return "unchecked-type-assertion"
71+
}
72+
73+
type lintUnchekedTypeAssertion struct {
74+
pkg *lint.Package
75+
onFailure func(lint.Failure)
76+
acceptIgnoredTypeAssertionResult bool
77+
}
78+
79+
func isIgnored(e ast.Expr) bool {
80+
ident, ok := e.(*ast.Ident)
81+
if !ok {
82+
return false
83+
}
84+
85+
return ident.Name == "_"
86+
}
87+
88+
func isTypeSwitch(e *ast.TypeAssertExpr) bool {
89+
return e.Type == nil
90+
}
91+
92+
func (w *lintUnchekedTypeAssertion) requireNoTypeAssert(expr ast.Expr) {
93+
e, ok := expr.(*ast.TypeAssertExpr)
94+
if ok && !isTypeSwitch(e) {
95+
w.addFailure(e, ruleUTAMessagePanic)
96+
}
97+
}
98+
99+
func (w *lintUnchekedTypeAssertion) handleIfStmt(n *ast.IfStmt) {
100+
ifCondition, ok := n.Cond.(*ast.BinaryExpr)
101+
if !ok {
102+
return
103+
}
104+
105+
w.requireNoTypeAssert(ifCondition.X)
106+
w.requireNoTypeAssert(ifCondition.Y)
107+
}
108+
109+
func (w *lintUnchekedTypeAssertion) requireBinaryExpressionWithoutTypeAssertion(expr ast.Expr) {
110+
binaryExpr, ok := expr.(*ast.BinaryExpr)
111+
if ok {
112+
w.requireNoTypeAssert(binaryExpr.X)
113+
w.requireNoTypeAssert(binaryExpr.Y)
114+
}
115+
}
116+
117+
func (w *lintUnchekedTypeAssertion) handleCaseClause(n *ast.CaseClause) {
118+
for _, expr := range n.List {
119+
w.requireNoTypeAssert(expr)
120+
w.requireBinaryExpressionWithoutTypeAssertion(expr)
121+
}
122+
}
123+
124+
func (w *lintUnchekedTypeAssertion) handleSwitch(n *ast.SwitchStmt) {
125+
w.requireNoTypeAssert(n.Tag)
126+
w.requireBinaryExpressionWithoutTypeAssertion(n.Tag)
127+
}
128+
129+
func (w *lintUnchekedTypeAssertion) handleAssignment(n *ast.AssignStmt) {
130+
if len(n.Rhs) == 0 {
131+
return
132+
}
133+
134+
e, ok := n.Rhs[0].(*ast.TypeAssertExpr)
135+
if !ok || e == nil {
136+
return
137+
}
138+
139+
if isTypeSwitch(e) {
140+
return
141+
}
142+
143+
if len(n.Lhs) == 1 {
144+
w.addFailure(e, ruleUTAMessagePanic)
145+
}
146+
147+
if !w.acceptIgnoredTypeAssertionResult && len(n.Lhs) == 2 && isIgnored(n.Lhs[1]) {
148+
w.addFailure(e, ruleUTAMessageIgnored)
149+
}
150+
}
151+
152+
// handles "return foo(.*bar)" - one of them is enough to fail as golang does not forward the type cast tuples in return statements
153+
func (w *lintUnchekedTypeAssertion) handleReturn(n *ast.ReturnStmt) {
154+
for _, r := range n.Results {
155+
w.requireNoTypeAssert(r)
156+
}
157+
}
158+
159+
func (w *lintUnchekedTypeAssertion) handleRange(n *ast.RangeStmt) {
160+
w.requireNoTypeAssert(n.X)
161+
}
162+
163+
func (w *lintUnchekedTypeAssertion) handleChannelSend(n *ast.SendStmt) {
164+
w.requireNoTypeAssert(n.Value)
165+
}
166+
167+
func (w *lintUnchekedTypeAssertion) Visit(node ast.Node) ast.Visitor {
168+
switch n := node.(type) {
169+
case *ast.RangeStmt:
170+
w.handleRange(n)
171+
case *ast.SwitchStmt:
172+
w.handleSwitch(n)
173+
case *ast.ReturnStmt:
174+
w.handleReturn(n)
175+
case *ast.AssignStmt:
176+
w.handleAssignment(n)
177+
case *ast.IfStmt:
178+
w.handleIfStmt(n)
179+
case *ast.CaseClause:
180+
w.handleCaseClause(n)
181+
case *ast.SendStmt:
182+
w.handleChannelSend(n)
183+
}
184+
185+
return w
186+
}
187+
188+
func (w *lintUnchekedTypeAssertion) addFailure(n *ast.TypeAssertExpr, why string) {
189+
s := fmt.Sprintf("type cast result is unchecked in %v - %s", gofmt(n), why)
190+
w.onFailure(lint.Failure{
191+
Category: "bad practice",
192+
Confidence: 1,
193+
Node: n,
194+
Failure: s,
195+
})
196+
}

test/unchecked_type_assertion_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/mgechev/revive/lint"
7+
"github.com/mgechev/revive/rule"
8+
)
9+
10+
func TestUncheckedDynamicCast(t *testing.T) {
11+
testRule(t, "unchecked-type-assertion", &rule.UncheckedTypeAssertionRule{})
12+
}
13+
14+
func TestUncheckedDynamicCastWithAcceptIgnored(t *testing.T) {
15+
args := []any{map[string]any{
16+
"acceptIgnoredAssertionResult": true,
17+
}}
18+
19+
testRule(t, "unchecked-type-assertion-accept-ignored", &rule.UncheckedTypeAssertionRule{}, &lint.RuleConfig{Arguments: args})
20+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package fixtures
2+
3+
var foo any = "foo"
4+
5+
func handleIgnoredIsOKByConfig() {
6+
// No lint here bacuse `acceptIgnoredAssertionResult` is set to `true`
7+
r, _ := foo.(int)
8+
}
9+
10+
func handleSkippedStillFails() {
11+
r := foo.(int) // MATCH /type cast result is unchecked in foo.(int) - type assertion will panic if not matched/
12+
}

0 commit comments

Comments
 (0)