Skip to content

Commit 6becd54

Browse files
authored
feature: new rule use-fmt-print (#1389)
print and println built-in functions are not recommended for use-cases other than language boostraping and are not guaranteed to stay in the language. This commit adds a new rule, use-fmt-print, that spots uses of print and println, and recommends using their fmt equivalents.
1 parent a260ed9 commit 6becd54

File tree

7 files changed

+177
-1
lines changed

7 files changed

+177
-1
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,7 @@ List of all available rules. The rules ported from `golint` are left unchanged a
549549
| [`unused-receiver`](./RULES_DESCRIPTIONS.md#unused-receiver) | n/a | Suggests to rename or remove unused method receivers | no | no |
550550
| [`use-any`](./RULES_DESCRIPTIONS.md#use-any) | n/a | Proposes to replace `interface{}` with its alias `any` | no | no |
551551
| [`use-errors-new`](./RULES_DESCRIPTIONS.md#use-errors-new) | n/a | Spots calls to `fmt.Errorf` that can be replaced by `errors.New` | no | no |
552+
| [`use-fmt-print`](./RULES_DESCRIPTIONS.md#use-fmt-print) | n/a | Proposes to replace calls to built-in `print` and `println` with their equivalents from `fmt`. | no | no |
552553
| [`useless-break`](./RULES_DESCRIPTIONS.md#useless-break) | n/a | Warns on useless `break` statements in case clauses | no | no |
553554
| [`var-declaration`](./RULES_DESCRIPTIONS.md#var-declaration) | n/a | Reduces redundancies around variable declaration. | yes | yes |
554555
| [`var-naming`](./RULES_DESCRIPTIONS.md#var-naming) | allowlist & blocklist of initialisms | Naming rules. | yes | no |

RULES_DESCRIPTIONS.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ List of all available rules.
8585
- [unused-receiver](#unused-receiver)
8686
- [use-any](#use-any)
8787
- [use-errors-new](#use-errors-new)
88+
- [use-fmt-print](#use-fmt-print)
8889
- [useless-break](#useless-break)
8990
- [var-declaration](#var-declaration)
9091
- [var-naming](#var-naming)
@@ -1287,7 +1288,16 @@ _Configuration_: N/A
12871288

12881289
## use-errors-new
12891290

1290-
_Description_: This rules identifies calls to `fmt.Errorf` that can be safely replaced by, the more efficient, `errors.New`.
1291+
_Description_: This rule identifies calls to `fmt.Errorf` that can be safely replaced by, the more efficient, `errors.New`.
1292+
1293+
_Configuration_: N/A
1294+
1295+
## use-fmt-print
1296+
1297+
_Description_: This rule proposes to replace calls to built-in `print` and `println` with their equivalents from `fmt` standard package.
1298+
1299+
`print` and `println` built-in functions are not recommended for use-cases other than
1300+
[language boostraping and are not guaranteed to stay in the language](https://go.dev/ref/spec#Bootstrapping).
12911301

12921302
_Configuration_: N/A
12931303

config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ var allRules = append([]lint.Rule{
103103
&rule.UseErrorsNewRule{},
104104
&rule.RedundantTestMainExitRule{},
105105
&rule.UnnecessaryFormatRule{},
106+
&rule.UseFmtPrintRule{},
106107
}, defaultRules...)
107108

108109
// allFormatters is a list of all available formatters to output the linting results.

rule/use_fmt_print.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package rule
2+
3+
import (
4+
"fmt"
5+
"go/ast"
6+
"strings"
7+
8+
"github.com/mgechev/revive/internal/astutils"
9+
"github.com/mgechev/revive/lint"
10+
)
11+
12+
// UseFmtPrintRule lints calls to print and println.
13+
type UseFmtPrintRule struct{}
14+
15+
// Apply applies the rule to given file.
16+
func (r *UseFmtPrintRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure {
17+
redefinesPrint, redefinesPrintln := r.analyzeRedefinitions(file.AST.Decls)
18+
// Here we could check if both are redefined and if it's the case return nil
19+
// but the check being false 99.9999999% of the cases we don't
20+
21+
var failures []lint.Failure
22+
onFailure := func(failure lint.Failure) {
23+
failures = append(failures, failure)
24+
}
25+
26+
w := lintUseFmtPrint{onFailure, redefinesPrint, redefinesPrintln}
27+
ast.Walk(w, file.AST)
28+
29+
return failures
30+
}
31+
32+
// Name returns the rule name.
33+
func (*UseFmtPrintRule) Name() string {
34+
return "use-fmt-print"
35+
}
36+
37+
type lintUseFmtPrint struct {
38+
onFailure func(lint.Failure)
39+
redefinesPrint bool
40+
redefinesPrintln bool
41+
}
42+
43+
func (w lintUseFmtPrint) Visit(node ast.Node) ast.Visitor {
44+
ce, ok := node.(*ast.CallExpr)
45+
if !ok {
46+
return w // nothing to do, the node is not a call
47+
}
48+
49+
id, ok := (ce.Fun).(*ast.Ident)
50+
if !ok {
51+
return nil
52+
}
53+
54+
name := id.Name
55+
switch name {
56+
case "print":
57+
if w.redefinesPrint {
58+
return nil // it's a call to user-defined print
59+
}
60+
case "println":
61+
if w.redefinesPrintln {
62+
return nil // it's a call to user-defined println
63+
}
64+
default:
65+
return nil // nothing to do, the call is not println(...) nor print(...)
66+
}
67+
68+
callArgs := w.callArgsAsStr(ce.Args)
69+
w.onFailure(lint.Failure{
70+
Confidence: 1,
71+
Node: node,
72+
Category: lint.FailureCategoryBadPractice,
73+
Failure: fmt.Sprintf(`avoid using built-in function %q, replace it by "fmt.F%s(os.Stderr, %s)"`, name, name, callArgs),
74+
})
75+
76+
return w
77+
}
78+
79+
func (lintUseFmtPrint) callArgsAsStr(args []ast.Expr) string {
80+
strs := []string{}
81+
for _, expr := range args {
82+
strs = append(strs, astutils.GoFmt(expr))
83+
}
84+
85+
return strings.Join(strs, ", ")
86+
}
87+
88+
func (UseFmtPrintRule) analyzeRedefinitions(decls []ast.Decl) (redefinesPrint, redefinesPrintln bool) {
89+
for _, decl := range decls {
90+
fnDecl, ok := decl.(*ast.FuncDecl)
91+
if !ok {
92+
continue // not a function declaration
93+
}
94+
95+
if fnDecl.Recv != nil {
96+
continue // it´s a method (not function) declaration
97+
}
98+
99+
switch fnDecl.Name.Name {
100+
case "print":
101+
redefinesPrint = true
102+
case "println":
103+
redefinesPrintln = true
104+
}
105+
}
106+
return redefinesPrint, redefinesPrintln
107+
}

test/use_fmt_print_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/mgechev/revive/rule"
7+
)
8+
9+
func TestUseFmtPrint(t *testing.T) {
10+
testRule(t, "use_fmt_print", &rule.UseFmtPrintRule{})
11+
testRule(t, "use_fmt_print_with_redefinition", &rule.UseFmtPrintRule{})
12+
}

testdata/use_fmt_print.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package fixtures
2+
3+
import (
4+
"fmt"
5+
)
6+
7+
type useFmtPrintT struct{}
8+
9+
func (useFmtPrintT) print(s string) {}
10+
func (useFmtPrintT) println(s string) {}
11+
12+
func useFmtPrint() {
13+
fmt.Println("just testing")
14+
fmt.Print("just testing")
15+
t := useFmtPrintT{}
16+
t.print("just testing")
17+
t.println("just testing")
18+
19+
println("just testing", something) // MATCH /avoid using built-in function "println", replace it by "fmt.Fprintln(os.Stderr, "just testing", something)"/
20+
print("just testing", some, thing+1) // MATCH /avoid using built-in function "print", replace it by "fmt.Fprint(os.Stderr, "just testing", some, thing + 1)"/
21+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package fixtures
2+
3+
import (
4+
"fmt"
5+
)
6+
7+
func print() {}
8+
func println() {}
9+
10+
type useFmtPrintT struct{}
11+
12+
func (useFmtPrintT) print(s string) {}
13+
func (useFmtPrintT) println(s string) {}
14+
15+
func useFmtPrint() {
16+
fmt.Println("just testing")
17+
fmt.Print("just testing")
18+
t := useFmtPrintT{}
19+
t.print("just testing")
20+
t.println("just testing")
21+
22+
println("just testing")
23+
print("just testing")
24+
}

0 commit comments

Comments
 (0)