@@ -17,14 +17,17 @@ limitations under the License.
1717package notimestamp
1818
1919import (
20+ "fmt"
2021 "go/ast"
22+ "go/token"
23+ "regexp"
2124 "strings"
2225
2326 "golang.org/x/tools/go/analysis"
24- "golang.org/x/tools/go/analysis/passes/inspect"
25- "golang.org/x/tools/go/ast/inspector"
2627 kalerrors "sigs.k8s.io/kube-api-linter/pkg/analysis/errors"
2728 "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/extractjsontags"
29+ "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/inspector"
30+ markershelper "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/markers"
2831 "sigs.k8s.io/kube-api-linter/pkg/analysis/utils"
2932)
3033
@@ -35,63 +38,75 @@ const name = "notimestamp"
3538// substring are present.
3639var Analyzer = & analysis.Analyzer {
3740 Name : name ,
38- Doc : "check that non of the struct field named timestamp or contain timestamp as a substring" ,
41+ Doc : "check that none of the struct field named timestamp or contain timestamp as a substring" ,
3942 Run : run ,
40- Requires : []* analysis.Analyzer {inspect . Analyzer , extractjsontags .Analyzer },
43+ Requires : []* analysis.Analyzer {inspector .Analyzer },
4144}
4245
46+ // case insensitive regular expression to match 'timestamp' string in field or json tag.
47+ var timeStampRegEx = regexp .MustCompile ("(?i)timestamp" )
48+
4349func run (pass * analysis.Pass ) (any , error ) {
44- inspect , ok := pass .ResultOf [inspect .Analyzer ].(* inspector.Inspector )
50+ inspect , ok := pass .ResultOf [inspector .Analyzer ].(inspector.Inspector )
4551 if ! ok {
4652 return nil , kalerrors .ErrCouldNotGetInspector
4753 }
4854
49- jsonTags , ok := pass .ResultOf [extractjsontags .Analyzer ].(extractjsontags.StructFieldTags )
50- if ! ok {
51- return nil , kalerrors .ErrCouldNotGetJSONTags
55+ inspect .InspectFields (func (field * ast.Field , stack []ast.Node , jsonTagInfo extractjsontags.FieldTagInfo , markersAccess markershelper.Markers ) {
56+ checkFieldsAndTags (pass , field , jsonTagInfo )
57+ })
58+
59+ return nil , nil //nolint:nilnil
60+ }
61+
62+ func checkFieldsAndTags (pass * analysis.Pass , field * ast.Field , tagInfo extractjsontags.FieldTagInfo ) {
63+ fieldName := utils .FieldName (field )
64+ if fieldName == "" {
65+ return
5266 }
5367
54- // Filter to fields so that we can iterate over fields in a struct.
55- nodeFilter := []ast.Node {
56- (* ast .Field )(nil ),
68+ var suggestedFixes []analysis.SuggestedFix
69+
70+ // check if filed name contains timestamp in it.
71+ fieldReplacementName := timeStampRegEx .ReplaceAllString (fieldName , "Time" )
72+ if fieldReplacementName != fieldName {
73+ suggestedFixes = append (suggestedFixes , analysis.SuggestedFix {
74+ Message : "replace field with Time" ,
75+ TextEdits : []analysis.TextEdit {
76+ {
77+ Pos : field .Pos (),
78+ NewText : []byte (fieldReplacementName ),
79+ End : field .Pos () + token .Pos (len (fieldName )),
80+ },
81+ },
82+ })
5783 }
5884
59- // Preorder visits all the nodes of the AST in depth-first order. It calls
60- // f(n) for each node n before it visits n's children.
61- //
62- // We use the filter defined above, ensuring we only look at struct fields.
63- inspect .Preorder (nodeFilter , func (n ast.Node ) {
64- field , ok := n .(* ast.Field )
65- if ! ok {
66- return
67- }
68-
69- if field == nil || len (field .Names ) == 0 {
70- return
71- }
72-
73- fieldName := utils .FieldName (field )
74-
75- // First check if the struct field name contains 'timestamp'
76- if strings .Contains (strings .ToLower (fieldName ), "timestamp" ) {
77- pass .Reportf (field .Pos (),
78- "field %s: fields with timestamp substring should be avoided" ,
79- fieldName ,
80- )
81-
82- return
83- }
84-
85- // Then check if the json serialization of the field contains 'timestamp'
86- tagInfo := jsonTags .FieldTags (field )
87-
88- if strings .Contains (strings .ToLower (tagInfo .Name ), "timestamp" ) {
89- pass .Reportf (field .Pos (),
90- "field %s: fields with timestamp substring should be avoided" ,
91- fieldName ,
92- )
93- }
94- })
85+ // check if the tag contains timestamp in it.
86+ tagReplacementName := timeStampRegEx .ReplaceAllString (tagInfo .Name , "Time" )
87+ if strings .HasPrefix (strings .ToLower (tagInfo .Name ), "time" ) {
88+ // If tag is starts with 'timeStamp', the replacement should be 'time' not 'Time'.
89+ tagReplacementName = timeStampRegEx .ReplaceAllString (tagInfo .Name , "time" )
90+ }
9591
96- return nil , nil //nolint:nilnil
92+ if tagReplacementName != tagInfo .Name {
93+ suggestedFixes = append (suggestedFixes , analysis.SuggestedFix {
94+ Message : "replace json tag with Time" ,
95+ TextEdits : []analysis.TextEdit {
96+ {
97+ Pos : tagInfo .Pos ,
98+ NewText : []byte (tagReplacementName ),
99+ End : tagInfo .Pos + token .Pos (len (tagInfo .Name )),
100+ },
101+ },
102+ })
103+ }
104+
105+ if len (suggestedFixes ) > 0 {
106+ pass .Report (analysis.Diagnostic {
107+ Pos : field .Pos (),
108+ Message : fmt .Sprintf ("field %s: prefer use of the term time over timestamp" , fieldName ),
109+ SuggestedFixes : suggestedFixes ,
110+ })
111+ }
97112}
0 commit comments