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/token" |
13 | "go/types" |
14 | "strings" |
15 | |
16 | "golang.org/x/tools/go/analysis" |
17 | "golang.org/x/tools/go/analysis/passes/inspect" |
18 | "golang.org/x/tools/go/ast/inspector" |
19 | "golang.org/x/tools/go/types/typeutil" |
20 | ) |
21 | |
22 | const badFormat = "2006-02-01" |
23 | const goodFormat = "2006-01-02" |
24 | |
25 | const Doc = `check for calls of (time.Time).Format or time.Parse with 2006-02-01 |
26 | |
27 | The timeformat checker looks for time formats with the 2006-02-01 (yyyy-dd-mm) |
28 | format. Internationally, "yyyy-dd-mm" does not occur in common calendar date |
29 | standards, and so it is more likely that 2006-01-02 (yyyy-mm-dd) was intended. |
30 | ` |
31 | |
32 | var Analyzer = &analysis.Analyzer{ |
33 | Name: "timeformat", |
34 | Doc: Doc, |
35 | Requires: []*analysis.Analyzer{inspect.Analyzer}, |
36 | Run: run, |
37 | } |
38 | |
39 | func run(pass *analysis.Pass) (interface{}, error) { |
40 | inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) |
41 | |
42 | nodeFilter := []ast.Node{ |
43 | (*ast.CallExpr)(nil), |
44 | } |
45 | inspect.Preorder(nodeFilter, func(n ast.Node) { |
46 | call := n.(*ast.CallExpr) |
47 | fn, ok := typeutil.Callee(pass.TypesInfo, call).(*types.Func) |
48 | if !ok { |
49 | return |
50 | } |
51 | if !isTimeDotFormat(fn) && !isTimeDotParse(fn) { |
52 | return |
53 | } |
54 | if len(call.Args) > 0 { |
55 | arg := call.Args[0] |
56 | badAt := badFormatAt(pass.TypesInfo, arg) |
57 | |
58 | if badAt > -1 { |
59 | // Check if it's a literal string, otherwise we can't suggest a fix. |
60 | if _, ok := arg.(*ast.BasicLit); ok { |
61 | pos := int(arg.Pos()) + badAt + 1 // +1 to skip the " or ` |
62 | end := pos + len(badFormat) |
63 | |
64 | pass.Report(analysis.Diagnostic{ |
65 | Pos: token.Pos(pos), |
66 | End: token.Pos(end), |
67 | Message: badFormat + " should be " + goodFormat, |
68 | SuggestedFixes: []analysis.SuggestedFix{{ |
69 | Message: "Replace " + badFormat + " with " + goodFormat, |
70 | TextEdits: []analysis.TextEdit{{ |
71 | Pos: token.Pos(pos), |
72 | End: token.Pos(end), |
73 | NewText: []byte(goodFormat), |
74 | }}, |
75 | }}, |
76 | }) |
77 | } else { |
78 | pass.Reportf(arg.Pos(), badFormat+" should be "+goodFormat) |
79 | } |
80 | } |
81 | } |
82 | }) |
83 | return nil, nil |
84 | } |
85 | |
86 | func isTimeDotFormat(f *types.Func) bool { |
87 | if f.Name() != "Format" || f.Pkg().Path() != "time" { |
88 | return false |
89 | } |
90 | sig, ok := f.Type().(*types.Signature) |
91 | if !ok { |
92 | return false |
93 | } |
94 | // Verify that the receiver is time.Time. |
95 | recv := sig.Recv() |
96 | if recv == nil { |
97 | return false |
98 | } |
99 | named, ok := recv.Type().(*types.Named) |
100 | return ok && named.Obj().Name() == "Time" |
101 | } |
102 | |
103 | func isTimeDotParse(f *types.Func) bool { |
104 | if f.Name() != "Parse" || f.Pkg().Path() != "time" { |
105 | return false |
106 | } |
107 | // Verify that there is no receiver. |
108 | sig, ok := f.Type().(*types.Signature) |
109 | return ok && sig.Recv() == nil |
110 | } |
111 | |
112 | // badFormatAt return the start of a bad format in e or -1 if no bad format is found. |
113 | func badFormatAt(info *types.Info, e ast.Expr) int { |
114 | tv, ok := info.Types[e] |
115 | if !ok { // no type info, assume good |
116 | return -1 |
117 | } |
118 | |
119 | t, ok := tv.Type.(*types.Basic) |
120 | if !ok || t.Info()&types.IsString == 0 { |
121 | return -1 |
122 | } |
123 | |
124 | if tv.Value == nil { |
125 | return -1 |
126 | } |
127 | |
128 | return strings.Index(constant.StringVal(tv.Value), badFormat) |
129 | } |
130 |
Members