forked from golang/tools
/
timeformat.go
133 lines (116 loc) · 3.48 KB
/
timeformat.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
// 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 (
_ "embed"
"go/ast"
"go/constant"
"go/token"
"go/types"
"strings"
"github.com/bafko/tools/go/analysis"
"github.com/bafko/tools/go/analysis/passes/inspect"
"github.com/bafko/tools/go/analysis/passes/internal/analysisutil"
"github.com/bafko/tools/go/ast/inspector"
"github.com/bafko/tools/go/types/typeutil"
)
const badFormat = "2006-02-01"
const goodFormat = "2006-01-02"
//go:embed doc.go
var doc string
var Analyzer = &analysis.Analyzer{
Name: "timeformat",
Doc: analysisutil.MustExtractDoc(doc, "timeformat"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/timeformat",
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run,
}
func run(pass *analysis.Pass) (interface{}, error) {
// Note: (time.Time).Format is a method and can be a typeutil.Callee
// without directly importing "time". So we cannot just skip this package
// when !analysisutil.Imports(pass.Pkg, "time").
// TODO(taking): Consider using a prepass to collect typeutil.Callees.
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 {
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)
}