Skip to content

Commit

Permalink
go/tools: add check for time formats with 2006-02-01
Browse files Browse the repository at this point in the history
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 <[email protected]>
TryBot-Result: Gopher Robot <[email protected]>
Reviewed-by: Tim King <[email protected]>
Run-TryBot: Tim King <[email protected]>
Reviewed-by: Robert Findley <[email protected]>
  • Loading branch information
erikdubbelboer authored and timothy-king committed Aug 3, 2022
1 parent d08f5dc commit 371fc67
Show file tree
Hide file tree
Showing 8 changed files with 282 additions and 0 deletions.
50 changes: 50 additions & 0 deletions go/analysis/passes/timeformat/testdata/src/a/a.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright 2022 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.

// This file contains tests for the timeformat checker.

package a

import (
"time"

"b"
)

func hasError() {
a, _ := time.Parse("2006-02-01 15:04:05", "2021-01-01 00:00:00") // want `2006-02-01 should be 2006-01-02`
a.Format(`2006-02-01`) // want `2006-02-01 should be 2006-01-02`
a.Format("2006-02-01 15:04:05") // want `2006-02-01 should be 2006-01-02`

const c = "2006-02-01"
a.Format(c) // want `2006-02-01 should be 2006-01-02`
}

func notHasError() {
a, _ := time.Parse("2006-01-02 15:04:05", "2021-01-01 00:00:00")
a.Format("2006-01-02")

const c = "2006-01-02"
a.Format(c)

v := "2006-02-01"
a.Format(v) // Allowed though variables.

m := map[string]string{
"y": "2006-02-01",
}
a.Format(m["y"])

s := []string{"2006-02-01"}
a.Format(s[0])

a.Format(badFormat())

o := b.Parse("2006-02-01 15:04:05", "2021-01-01 00:00:00")
o.Format("2006-02-01")
}

func badFormat() string {
return "2006-02-01"
}
50 changes: 50 additions & 0 deletions go/analysis/passes/timeformat/testdata/src/a/a.go.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright 2022 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.

// This file contains tests for the timeformat checker.

package a

import (
"time"

"b"
)

func hasError() {
a, _ := time.Parse("2006-01-02 15:04:05", "2021-01-01 00:00:00") // want `2006-02-01 should be 2006-01-02`
a.Format(`2006-01-02`) // want `2006-02-01 should be 2006-01-02`
a.Format("2006-01-02 15:04:05") // want `2006-02-01 should be 2006-01-02`

const c = "2006-02-01"
a.Format(c) // want `2006-02-01 should be 2006-01-02`
}

func notHasError() {
a, _ := time.Parse("2006-01-02 15:04:05", "2021-01-01 00:00:00")
a.Format("2006-01-02")

const c = "2006-01-02"
a.Format(c)

v := "2006-02-01"
a.Format(v) // Allowed though variables.

m := map[string]string{
"y": "2006-02-01",
}
a.Format(m["y"])

s := []string{"2006-02-01"}
a.Format(s[0])

a.Format(badFormat())

o := b.Parse("2006-02-01 15:04:05", "2021-01-01 00:00:00")
o.Format("2006-02-01")
}

func badFormat() string {
return "2006-02-01"
}
11 changes: 11 additions & 0 deletions go/analysis/passes/timeformat/testdata/src/b/b.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package b

type B struct {
}

func Parse(string, string) B {
return B{}
}

func (b B) Format(string) {
}
131 changes: 131 additions & 0 deletions go/analysis/passes/timeformat/timeformat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// Copyright 2022 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 timeformat defines an Analyzer that checks for the use
// of time.Format or time.Parse calls with a bad format.
package timeformat

import (
"fmt"
"go/ast"
"go/constant"
"go/token"
"go/types"
"strings"

"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/go/types/typeutil"
)

const badFormat = "2006-02-01"
const goodFormat = "2006-01-02"

const Doc = `check for calls of (time.Time).Format or time.Parse with 2006-02-01
The timeformat checker looks for time formats with the 2006-02-01 (yyyy-dd-mm)
format. Internationally, "yyyy-dd-mm" does not occur in common calendar date
standards, and so it is more likely that 2006-01-02 (yyyy-mm-dd) was intended.
`

var Analyzer = &analysis.Analyzer{
Name: "timeformat",
Doc: Doc,
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run,
}

func run(pass *analysis.Pass) (interface{}, error) {
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)

nodeFilter := []ast.Node{
(*ast.CallExpr)(nil),
}
inspect.Preorder(nodeFilter, func(n ast.Node) {
call := n.(*ast.CallExpr)
fn, ok := typeutil.Callee(pass.TypesInfo, call).(*types.Func)
if !ok {
return
}
if !isTimeDotFormat(fn) && !isTimeDotParse(fn) {
return
}
if len(call.Args) > 0 {
arg := call.Args[0]
badAt := badFormatAt(pass.TypesInfo, arg)

if badAt > -1 {
// Check if it's a literal string, otherwise we can't suggest a fix.
if _, ok := arg.(*ast.BasicLit); ok {
fmt.Printf("%#v\n", arg)
pos := int(arg.Pos()) + badAt + 1 // +1 to skip the " or `
end := pos + len(badFormat)

pass.Report(analysis.Diagnostic{
Pos: token.Pos(pos),
End: token.Pos(end),
Message: badFormat + " should be " + goodFormat,
SuggestedFixes: []analysis.SuggestedFix{{
Message: "Replace " + badFormat + " with " + goodFormat,
TextEdits: []analysis.TextEdit{{
Pos: token.Pos(pos),
End: token.Pos(end),
NewText: []byte(goodFormat),
}},
}},
})
} else {
pass.Reportf(arg.Pos(), badFormat+" should be "+goodFormat)
}
}
}
})
return nil, nil
}

func isTimeDotFormat(f *types.Func) bool {
if f.Name() != "Format" || f.Pkg().Path() != "time" {
return false
}
sig, ok := f.Type().(*types.Signature)
if !ok {
return false
}
// Verify that the receiver is time.Time.
recv := sig.Recv()
if recv == nil {
return false
}
named, ok := recv.Type().(*types.Named)
return ok && named.Obj().Name() == "Time"
}

func isTimeDotParse(f *types.Func) bool {
if f.Name() != "Parse" || f.Pkg().Path() != "time" {
return false
}
// Verify that there is no receiver.
sig, ok := f.Type().(*types.Signature)
return ok && sig.Recv() == nil
}

// badFormatAt return the start of a bad format in e or -1 if no bad format is found.
func badFormatAt(info *types.Info, e ast.Expr) int {
tv, ok := info.Types[e]
if !ok { // no type info, assume good
return -1
}

t, ok := tv.Type.(*types.Basic)
if !ok || t.Info()&types.IsString == 0 {
return -1
}

if tv.Value == nil {
return -1
}

return strings.Index(constant.StringVal(tv.Value), badFormat)
}
17 changes: 17 additions & 0 deletions go/analysis/passes/timeformat/timeformat_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright 2022 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 timeformat_test

import (
"testing"

"golang.org/x/tools/go/analysis/analysistest"
"golang.org/x/tools/go/analysis/passes/timeformat"
)

func Test(t *testing.T) {
testdata := analysistest.TestData()
analysistest.RunWithSuggestedFixes(t, testdata, timeformat.Analyzer, "a")
}
11 changes: 11 additions & 0 deletions gopls/doc/analyzers.md
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,17 @@ identifiers.
Please see the documentation for package testing in golang.org/pkg/testing
for the conventions that are enforced for Tests, Benchmarks, and Examples.

**Enabled by default.**

## **timeformat**

check for calls of (time.Time).Format or time.Parse with 2006-02-01

The timeformat checker looks for time formats with the 2006-02-01 (yyyy-dd-mm)
format. Internationally, "yyyy-dd-mm" does not occur in common calendar date
standards, and so it is more likely that 2006-01-02 (yyyy-mm-dd) was intended.


**Enabled by default.**

## **unmarshal**
Expand Down
10 changes: 10 additions & 0 deletions internal/lsp/source/api_json.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions internal/lsp/source/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import (
"golang.org/x/tools/go/analysis/passes/structtag"
"golang.org/x/tools/go/analysis/passes/testinggoroutine"
"golang.org/x/tools/go/analysis/passes/tests"
"golang.org/x/tools/go/analysis/passes/timeformat"
"golang.org/x/tools/go/analysis/passes/unmarshal"
"golang.org/x/tools/go/analysis/passes/unreachable"
"golang.org/x/tools/go/analysis/passes/unsafeptr"
Expand Down Expand Up @@ -1393,6 +1394,7 @@ func defaultAnalyzers() map[string]*Analyzer {
useany.Analyzer.Name: {Analyzer: useany.Analyzer, Enabled: false},
infertypeargs.Analyzer.Name: {Analyzer: infertypeargs.Analyzer, Enabled: true},
embeddirective.Analyzer.Name: {Analyzer: embeddirective.Analyzer, Enabled: true},
timeformat.Analyzer.Name: {Analyzer: timeformat.Analyzer, Enabled: true},

// gofmt -s suite:
simplifycompositelit.Analyzer.Name: {
Expand Down

0 comments on commit 371fc67

Please sign in to comment.