diff --git a/README.md b/README.md index 498c6b0..52a9bd6 100644 --- a/README.md +++ b/README.md @@ -347,6 +347,7 @@ List of all available rules. The rules ported from `golint` are left unchanged a | [`unused-receiver`](./RULES_DESCRIPTIONS.md#unused-receiver) | n/a | Suggests to rename or remove unused method receivers | no | no | | [`unhandled-error`](./RULES_DESCRIPTIONS.md#unhandled-error) | []string | Warns on unhandled errors returned by funcion calls | no | yes | | [`cognitive-complexity`](./RULES_DESCRIPTIONS.md#cognitive-complexity) | int | Sets restriction for maximum Cognitive complexity. | no | no | +| [`string-of-int`](./RULES_DESCRIPTIONS.md#string-of-int) | n/a | Warns on suspicious casts from int to string | no | yes | ## Configurable rules diff --git a/RULES_DESCRIPTIONS.md b/RULES_DESCRIPTIONS.md index be1857e..96bd62a 100644 --- a/RULES_DESCRIPTIONS.md +++ b/RULES_DESCRIPTIONS.md @@ -46,6 +46,7 @@ List of all available rules. - [range-val-in-closure](#range-val-in-closure) - [receiver-naming](#receiver-naming) - [redefines-builtin-id](#redefines-builtin-id) + - [string-of-int](#string-of-int) - [struct-tag](#struct-tag) - [superfluous-else](#superfluous-else) - [time-naming](#time-naming) @@ -406,6 +407,11 @@ Even if possible, redefining these built in names can lead to bugs very difficul _Configuration_: N/A +## string-of-int +_Description_: explicit type conversion `string(i)` where `i` has an integer type other than `rune` might behave not as expected by the developer (e.g. `string(42)` is not `"42"`). This rule spot that kind of suspicious conversions. + +_Configuration_: N/A + ## struct-tag _Description_: Struct tags are not checked at compile time. diff --git a/config.go b/config.go index 06863fd..6e55f9c 100644 --- a/config.go +++ b/config.go @@ -81,6 +81,7 @@ var allRules = append([]lint.Rule{ &rule.UnusedReceiverRule{}, &rule.UnhandledErrorRule{}, &rule.CognitiveComplexityRule{}, + &rule.StringOfIntRule{}, }, defaultRules...) var allFormatters = []lint.Formatter{ diff --git a/fixtures/string-of-int.go b/fixtures/string-of-int.go new file mode 100644 index 0000000..8efd46d --- /dev/null +++ b/fixtures/string-of-int.go @@ -0,0 +1,30 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +package fixtures + +type A string +type B = string +type C int +type D = uintptr + +func StringTest() { + var ( + i int + j rune + k byte + l C + m D + n = []int{0, 1, 2} + o struct{ x int } + ) + const p = 0 + _ = string(i) // MATCH /dubious convertion of an integer into a string, use strconv.Itoa/ + _ = string(j) + _ = string(k) + _ = string(p) // MATCH /dubious convertion of an integer into a string, use strconv.Itoa/ + _ = A(l) // MATCH /dubious convertion of an integer into a string, use strconv.Itoa/ + _ = B(m) // MATCH /dubious convertion of an integer into a string, use strconv.Itoa/ + _ = string(n[1]) // MATCH /dubious convertion of an integer into a string, use strconv.Itoa/ + _ = string(o.x) // MATCH /dubious convertion of an integer into a string, use strconv.Itoa/ +} diff --git a/rule/string-of-int.go b/rule/string-of-int.go new file mode 100644 index 0000000..38f453a --- /dev/null +++ b/rule/string-of-int.go @@ -0,0 +1,95 @@ +package rule + +import ( + "go/ast" + "go/types" + + "github.com/mgechev/revive/lint" +) + +// StringOfIntRule warns when logic expressions contains Boolean literals. +type StringOfIntRule struct{} + +// Apply applies the rule to given file. +func (r *StringOfIntRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure { + var failures []lint.Failure + + onFailure := func(failure lint.Failure) { + failures = append(failures, failure) + } + + astFile := file.AST + file.Pkg.TypeCheck() + + w := &lintStringInt{file, onFailure} + ast.Walk(w, astFile) + + return failures +} + +// Name returns the rule name. +func (r *StringOfIntRule) Name() string { + return "string-of-int" +} + +type lintStringInt struct { + file *lint.File + onFailure func(lint.Failure) +} + +func (w *lintStringInt) Visit(node ast.Node) ast.Visitor { + ce, ok := node.(*ast.CallExpr) + if !ok { + return w + } + + if !w.isCallStringCast(ce.Fun) { + return w + } + + if !w.isIntExpression(ce.Args) { + return w + } + + w.onFailure(lint.Failure{ + Confidence: 1, + Node: ce, + Failure: "dubious convertion of an integer into a string, use strconv.Itoa", + }) + + return w +} + +func (w *lintStringInt) isCallStringCast(e ast.Expr) bool { + t := w.file.Pkg.TypeOf(e) + if t == nil { + return false + } + + tb, _ := t.Underlying().(*types.Basic) + + return tb != nil && tb.Kind() == types.String +} + +func (w *lintStringInt) isIntExpression(es []ast.Expr) bool { + if len(es) != 1 { + return false + } + + t := w.file.Pkg.TypeOf(es[0]) + if t == nil { + return false + } + + ut, _ := t.Underlying().(*types.Basic) + if ut == nil || ut.Info()&types.IsInteger == 0 { + return false + } + + switch ut.Kind() { + case types.Byte, types.Rune, types.UntypedRune: + return false + } + + return true +} diff --git a/test/string-of-int.go b/test/string-of-int.go new file mode 100644 index 0000000..dc756d8 --- /dev/null +++ b/test/string-of-int.go @@ -0,0 +1,12 @@ +package test + +import ( + "testing" + + "github.com/mgechev/revive/rule" +) + +// String-of-int rule. +func TestStringOfInt(t *testing.T) { + testRule(t, "string-of-int", &rule.StringOfIntRule{}) +}