Skip to content

Commit 371fc67

Browse files
erikdubbelboertimothy-king
authored andcommitted
go/tools: add check for time formats with 2006-02-01
yyyy-dd-mm is a time format that isn't really used anywhere [1]. It is much more likely that the user intended to use yyyy-mm-dd instead and made a mistake. This happens quite often [2] because of the unusual way to handle time formatting and parsing in Go. Since the mistake is Go specific and happens so often a vet check will be useful. 1. https://stackoverflow.com/questions/2254014/are-there-locales-or-common-programs-that-use-yyyy-dd-mm-as-the-date-format 2. https://github.com/search?l=&p=1&q=%222006-02-01%22+language%3AGo&type=Code Updates golang/go#48801 Change-Id: I20960c93710766f20a7df90873bff960dea41b28 GitHub-Last-Rev: 496b991 GitHub-Pull-Request: #342 Reviewed-on: https://go-review.googlesource.com/c/tools/+/354010 gopls-CI: kokoro <noreply+kokoro@google.com> TryBot-Result: Gopher Robot <gobot@golang.org> Reviewed-by: Tim King <taking@google.com> Run-TryBot: Tim King <taking@google.com> Reviewed-by: Robert Findley <rfindley@google.com>
1 parent d08f5dc commit 371fc67

File tree

8 files changed

+282
-0
lines changed

8 files changed

+282
-0
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright 2022 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// This file contains tests for the timeformat checker.
6+
7+
package a
8+
9+
import (
10+
"time"
11+
12+
"b"
13+
)
14+
15+
func hasError() {
16+
a, _ := time.Parse("2006-02-01 15:04:05", "2021-01-01 00:00:00") // want `2006-02-01 should be 2006-01-02`
17+
a.Format(`2006-02-01`) // want `2006-02-01 should be 2006-01-02`
18+
a.Format("2006-02-01 15:04:05") // want `2006-02-01 should be 2006-01-02`
19+
20+
const c = "2006-02-01"
21+
a.Format(c) // want `2006-02-01 should be 2006-01-02`
22+
}
23+
24+
func notHasError() {
25+
a, _ := time.Parse("2006-01-02 15:04:05", "2021-01-01 00:00:00")
26+
a.Format("2006-01-02")
27+
28+
const c = "2006-01-02"
29+
a.Format(c)
30+
31+
v := "2006-02-01"
32+
a.Format(v) // Allowed though variables.
33+
34+
m := map[string]string{
35+
"y": "2006-02-01",
36+
}
37+
a.Format(m["y"])
38+
39+
s := []string{"2006-02-01"}
40+
a.Format(s[0])
41+
42+
a.Format(badFormat())
43+
44+
o := b.Parse("2006-02-01 15:04:05", "2021-01-01 00:00:00")
45+
o.Format("2006-02-01")
46+
}
47+
48+
func badFormat() string {
49+
return "2006-02-01"
50+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright 2022 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// This file contains tests for the timeformat checker.
6+
7+
package a
8+
9+
import (
10+
"time"
11+
12+
"b"
13+
)
14+
15+
func hasError() {
16+
a, _ := time.Parse("2006-01-02 15:04:05", "2021-01-01 00:00:00") // want `2006-02-01 should be 2006-01-02`
17+
a.Format(`2006-01-02`) // want `2006-02-01 should be 2006-01-02`
18+
a.Format("2006-01-02 15:04:05") // want `2006-02-01 should be 2006-01-02`
19+
20+
const c = "2006-02-01"
21+
a.Format(c) // want `2006-02-01 should be 2006-01-02`
22+
}
23+
24+
func notHasError() {
25+
a, _ := time.Parse("2006-01-02 15:04:05", "2021-01-01 00:00:00")
26+
a.Format("2006-01-02")
27+
28+
const c = "2006-01-02"
29+
a.Format(c)
30+
31+
v := "2006-02-01"
32+
a.Format(v) // Allowed though variables.
33+
34+
m := map[string]string{
35+
"y": "2006-02-01",
36+
}
37+
a.Format(m["y"])
38+
39+
s := []string{"2006-02-01"}
40+
a.Format(s[0])
41+
42+
a.Format(badFormat())
43+
44+
o := b.Parse("2006-02-01 15:04:05", "2021-01-01 00:00:00")
45+
o.Format("2006-02-01")
46+
}
47+
48+
func badFormat() string {
49+
return "2006-02-01"
50+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package b
2+
3+
type B struct {
4+
}
5+
6+
func Parse(string, string) B {
7+
return B{}
8+
}
9+
10+
func (b B) Format(string) {
11+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
// Copyright 2022 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// Package timeformat defines an Analyzer that checks for the use
6+
// of time.Format or time.Parse calls with a bad format.
7+
package timeformat
8+
9+
import (
10+
"fmt"
11+
"go/ast"
12+
"go/constant"
13+
"go/token"
14+
"go/types"
15+
"strings"
16+
17+
"golang.org/x/tools/go/analysis"
18+
"golang.org/x/tools/go/analysis/passes/inspect"
19+
"golang.org/x/tools/go/ast/inspector"
20+
"golang.org/x/tools/go/types/typeutil"
21+
)
22+
23+
const badFormat = "2006-02-01"
24+
const goodFormat = "2006-01-02"
25+
26+
const Doc = `check for calls of (time.Time).Format or time.Parse with 2006-02-01
27+
28+
The timeformat checker looks for time formats with the 2006-02-01 (yyyy-dd-mm)
29+
format. Internationally, "yyyy-dd-mm" does not occur in common calendar date
30+
standards, and so it is more likely that 2006-01-02 (yyyy-mm-dd) was intended.
31+
`
32+
33+
var Analyzer = &analysis.Analyzer{
34+
Name: "timeformat",
35+
Doc: Doc,
36+
Requires: []*analysis.Analyzer{inspect.Analyzer},
37+
Run: run,
38+
}
39+
40+
func run(pass *analysis.Pass) (interface{}, error) {
41+
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
42+
43+
nodeFilter := []ast.Node{
44+
(*ast.CallExpr)(nil),
45+
}
46+
inspect.Preorder(nodeFilter, func(n ast.Node) {
47+
call := n.(*ast.CallExpr)
48+
fn, ok := typeutil.Callee(pass.TypesInfo, call).(*types.Func)
49+
if !ok {
50+
return
51+
}
52+
if !isTimeDotFormat(fn) && !isTimeDotParse(fn) {
53+
return
54+
}
55+
if len(call.Args) > 0 {
56+
arg := call.Args[0]
57+
badAt := badFormatAt(pass.TypesInfo, arg)
58+
59+
if badAt > -1 {
60+
// Check if it's a literal string, otherwise we can't suggest a fix.
61+
if _, ok := arg.(*ast.BasicLit); ok {
62+
fmt.Printf("%#v\n", arg)
63+
pos := int(arg.Pos()) + badAt + 1 // +1 to skip the " or `
64+
end := pos + len(badFormat)
65+
66+
pass.Report(analysis.Diagnostic{
67+
Pos: token.Pos(pos),
68+
End: token.Pos(end),
69+
Message: badFormat + " should be " + goodFormat,
70+
SuggestedFixes: []analysis.SuggestedFix{{
71+
Message: "Replace " + badFormat + " with " + goodFormat,
72+
TextEdits: []analysis.TextEdit{{
73+
Pos: token.Pos(pos),
74+
End: token.Pos(end),
75+
NewText: []byte(goodFormat),
76+
}},
77+
}},
78+
})
79+
} else {
80+
pass.Reportf(arg.Pos(), badFormat+" should be "+goodFormat)
81+
}
82+
}
83+
}
84+
})
85+
return nil, nil
86+
}
87+
88+
func isTimeDotFormat(f *types.Func) bool {
89+
if f.Name() != "Format" || f.Pkg().Path() != "time" {
90+
return false
91+
}
92+
sig, ok := f.Type().(*types.Signature)
93+
if !ok {
94+
return false
95+
}
96+
// Verify that the receiver is time.Time.
97+
recv := sig.Recv()
98+
if recv == nil {
99+
return false
100+
}
101+
named, ok := recv.Type().(*types.Named)
102+
return ok && named.Obj().Name() == "Time"
103+
}
104+
105+
func isTimeDotParse(f *types.Func) bool {
106+
if f.Name() != "Parse" || f.Pkg().Path() != "time" {
107+
return false
108+
}
109+
// Verify that there is no receiver.
110+
sig, ok := f.Type().(*types.Signature)
111+
return ok && sig.Recv() == nil
112+
}
113+
114+
// badFormatAt return the start of a bad format in e or -1 if no bad format is found.
115+
func badFormatAt(info *types.Info, e ast.Expr) int {
116+
tv, ok := info.Types[e]
117+
if !ok { // no type info, assume good
118+
return -1
119+
}
120+
121+
t, ok := tv.Type.(*types.Basic)
122+
if !ok || t.Info()&types.IsString == 0 {
123+
return -1
124+
}
125+
126+
if tv.Value == nil {
127+
return -1
128+
}
129+
130+
return strings.Index(constant.StringVal(tv.Value), badFormat)
131+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright 2022 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package timeformat_test
6+
7+
import (
8+
"testing"
9+
10+
"golang.org/x/tools/go/analysis/analysistest"
11+
"golang.org/x/tools/go/analysis/passes/timeformat"
12+
)
13+
14+
func Test(t *testing.T) {
15+
testdata := analysistest.TestData()
16+
analysistest.RunWithSuggestedFixes(t, testdata, timeformat.Analyzer, "a")
17+
}

gopls/doc/analyzers.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,17 @@ identifiers.
495495
Please see the documentation for package testing in golang.org/pkg/testing
496496
for the conventions that are enforced for Tests, Benchmarks, and Examples.
497497

498+
**Enabled by default.**
499+
500+
## **timeformat**
501+
502+
check for calls of (time.Time).Format or time.Parse with 2006-02-01
503+
504+
The timeformat checker looks for time formats with the 2006-02-01 (yyyy-dd-mm)
505+
format. Internationally, "yyyy-dd-mm" does not occur in common calendar date
506+
standards, and so it is more likely that 2006-01-02 (yyyy-mm-dd) was intended.
507+
508+
498509
**Enabled by default.**
499510

500511
## **unmarshal**

internal/lsp/source/api_json.go

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/lsp/source/options.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import (
4343
"golang.org/x/tools/go/analysis/passes/structtag"
4444
"golang.org/x/tools/go/analysis/passes/testinggoroutine"
4545
"golang.org/x/tools/go/analysis/passes/tests"
46+
"golang.org/x/tools/go/analysis/passes/timeformat"
4647
"golang.org/x/tools/go/analysis/passes/unmarshal"
4748
"golang.org/x/tools/go/analysis/passes/unreachable"
4849
"golang.org/x/tools/go/analysis/passes/unsafeptr"
@@ -1393,6 +1394,7 @@ func defaultAnalyzers() map[string]*Analyzer {
13931394
useany.Analyzer.Name: {Analyzer: useany.Analyzer, Enabled: false},
13941395
infertypeargs.Analyzer.Name: {Analyzer: infertypeargs.Analyzer, Enabled: true},
13951396
embeddirective.Analyzer.Name: {Analyzer: embeddirective.Analyzer, Enabled: true},
1397+
timeformat.Analyzer.Name: {Analyzer: timeformat.Analyzer, Enabled: true},
13961398

13971399
// gofmt -s suite:
13981400
simplifycompositelit.Analyzer.Name: {

0 commit comments

Comments
 (0)