Skip to content

Commit 1396411

Browse files
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
1 parent 0e859af commit 1396411

File tree

7 files changed

+201
-0
lines changed

7 files changed

+201
-0
lines changed
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+
}
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+
}
+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
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+
"go/ast"
11+
"go/constant"
12+
"go/types"
13+
"strings"
14+
15+
"golang.org/x/tools/go/analysis"
16+
"golang.org/x/tools/go/analysis/passes/inspect"
17+
"golang.org/x/tools/go/ast/inspector"
18+
"golang.org/x/tools/go/types/typeutil"
19+
)
20+
21+
const Doc = `check for calls of (time.Time).Format or time.Parse with 2006-02-01
22+
23+
The timeformat checker looks for time formats with the 2006-02-01 (yyyy-dd-mm)
24+
format. Internationally, "yyyy-dd-mm" does not occur in common calendar date
25+
standards, and so it is more likely that 2006-01-02 (yyyy-mm-dd) was intended.
26+
`
27+
28+
var Analyzer = &analysis.Analyzer{
29+
Name: "timeformat",
30+
Doc: Doc,
31+
Requires: []*analysis.Analyzer{inspect.Analyzer},
32+
Run: run,
33+
}
34+
35+
func run(pass *analysis.Pass) (interface{}, error) {
36+
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
37+
38+
nodeFilter := []ast.Node{
39+
(*ast.CallExpr)(nil),
40+
}
41+
inspect.Preorder(nodeFilter, func(n ast.Node) {
42+
call := n.(*ast.CallExpr)
43+
fn, ok := typeutil.Callee(pass.TypesInfo, call).(*types.Func)
44+
if !ok {
45+
return
46+
}
47+
if !isTimeDotFormat(fn) && !isTimeDotParse(fn) {
48+
return
49+
}
50+
if len(call.Args) > 0 && isBadFormat(pass.TypesInfo, call.Args[0]) {
51+
pass.ReportRangef(call, "2006-02-01 should be 2006-01-02")
52+
}
53+
})
54+
return nil, nil
55+
}
56+
57+
func isTimeDotFormat(f *types.Func) bool {
58+
if f.Name() != "Format" || f.Pkg().Path() != "time" {
59+
return false
60+
}
61+
sig, ok := f.Type().(*types.Signature)
62+
if !ok {
63+
return false
64+
}
65+
// Verify that the receiver is time.Time.
66+
recv := sig.Recv()
67+
if recv == nil {
68+
return false
69+
}
70+
named, ok := recv.Type().(*types.Named)
71+
return ok && named.Obj().Name() == "Time"
72+
}
73+
74+
func isTimeDotParse(f *types.Func) bool {
75+
if f.Name() != "Parse" || f.Pkg().Path() != "time" {
76+
return false
77+
}
78+
// Verify that there is no receiver.
79+
sig, ok := f.Type().(*types.Signature)
80+
return ok && sig.Recv() == nil
81+
}
82+
83+
// isBadFormat return true when e is a string containing 2006-02-01.
84+
func isBadFormat(info *types.Info, e ast.Expr) bool {
85+
tv, ok := info.Types[e]
86+
if !ok { // no type info, assume good
87+
return false
88+
}
89+
90+
t, ok := tv.Type.(*types.Basic)
91+
if !ok || t.Info()&types.IsString == 0 {
92+
return false
93+
}
94+
95+
if tv.Value == nil {
96+
return false
97+
}
98+
99+
return strings.Contains(constant.StringVal(tv.Value), "2006-02-01")
100+
}
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.Run(t, testdata, timeformat.Analyzer, "a")
17+
}

gopls/doc/analyzers.md

+11
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,17 @@ identifiers.
490490
Please see the documentation for package testing in golang.org/pkg/testing
491491
for the conventions that are enforced for Tests, Benchmarks, and Examples.
492492

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

495506
## **unmarshal**

internal/lsp/source/api_json.go

+10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/lsp/source/options.go

+2
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"
@@ -1319,6 +1320,7 @@ func defaultAnalyzers() map[string]*Analyzer {
13191320
useany.Analyzer.Name: {Analyzer: useany.Analyzer, Enabled: false},
13201321
infertypeargs.Analyzer.Name: {Analyzer: infertypeargs.Analyzer, Enabled: true},
13211322
embeddirective.Analyzer.Name: {Analyzer: embeddirective.Analyzer, Enabled: true},
1323+
timeformat.Analyzer.Name: {Analyzer: timeformat.Analyzer, Enabled: true},
13221324

13231325
// gofmt -s suite:
13241326
simplifycompositelit.Analyzer.Name: {

0 commit comments

Comments
 (0)