Skip to content

Commit 1b29e82

Browse files
authored
Merge pull request #105 from Karthik-K-N/add-notimestamp
Add notimestamp linter
2 parents 8eacb16 + b461f66 commit 1b29e82

File tree

9 files changed

+400
-0
lines changed

9 files changed

+400
-0
lines changed

docs/linters.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
- [NoFloats](#nofloats) - Prevents usage of floating-point types
1111
- [Nomaps](#nomaps) - Restricts usage of map types
1212
- [Nophase](#nophase) - Prevents usage of 'Phase' fields
13+
- [Notimestamp](#notimestamp) - Prevents usage of 'TimeStamp' fields
1314
- [OptionalFields](#optionalfields) - Validates optional field conventions
1415
- [OptionalOrRequired](#optionalorrequired) - Ensures fields are explicitly marked as optional or required
1516
- [RequiredFields](#requiredfields) - Validates required field conventions
@@ -160,6 +161,19 @@ lintersConfig:
160161
policy: Enforce | AllowStringToStringMaps | Ignore # Determines how the linter should handle maps of simple types. Defaults to AllowStringToStringMaps.
161162
```
162163

164+
## Notimestamp
165+
166+
The `notimestamp` linter checks that the fields in the API are not named with the word 'Timestamp'.
167+
168+
The name of a field that specifies the time at which something occurs should be called `somethingTime`. It is recommended not use 'stamp' (e.g., creationTimestamp).
169+
170+
### Fixes
171+
172+
The `notimestamp` linter will automatically fix fields and json tags that are named with the word 'Timestamp'.
173+
174+
It will automatically replace 'Timestamp' with 'Time' and update both the field and tag name.
175+
Example: 'FooTimestamp' will be updated to 'FooTime'.
176+
163177
## Nophase
164178

165179
The `nophase` linter checks that the fields in the API types don't contain a 'Phase', or any field which contains 'Phase' as a substring, e.g MachinePhase.

pkg/analysis/notimestamp/analyzer.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package notimestamp
18+
19+
import (
20+
"fmt"
21+
"go/ast"
22+
"go/token"
23+
"regexp"
24+
"strings"
25+
26+
"golang.org/x/tools/go/analysis"
27+
kalerrors "sigs.k8s.io/kube-api-linter/pkg/analysis/errors"
28+
"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"
31+
"sigs.k8s.io/kube-api-linter/pkg/analysis/utils"
32+
)
33+
34+
const name = "notimestamp"
35+
36+
// Analyzer is the analyzer for the notimestamp package.
37+
// It checks that no struct fields named 'timestamp', or that contain timestamp as a
38+
// substring are present.
39+
var Analyzer = &analysis.Analyzer{
40+
Name: name,
41+
Doc: "Suggest the usage of the term 'time' over 'timestamp'",
42+
Run: run,
43+
Requires: []*analysis.Analyzer{inspector.Analyzer},
44+
}
45+
46+
// case-insensitive regular expression to match 'timestamp' string in field or json tag.
47+
var timeStampRegEx = regexp.MustCompile("(?i)timestamp")
48+
49+
func run(pass *analysis.Pass) (any, error) {
50+
inspect, ok := pass.ResultOf[inspector.Analyzer].(inspector.Inspector)
51+
if !ok {
52+
return nil, kalerrors.ErrCouldNotGetInspector
53+
}
54+
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
66+
}
67+
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: fmt.Sprintf("replace %s with %s", fieldName, fieldReplacementName),
75+
TextEdits: []analysis.TextEdit{
76+
{
77+
Pos: field.Pos(),
78+
NewText: []byte(fieldReplacementName),
79+
End: field.Pos() + token.Pos(len(fieldName)),
80+
},
81+
},
82+
})
83+
}
84+
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 the tag starts with 'timeStamp', the replacement should be 'time' not 'Time'.
89+
tagReplacementName = timeStampRegEx.ReplaceAllString(tagInfo.Name, "time")
90+
}
91+
92+
if tagReplacementName != tagInfo.Name {
93+
suggestedFixes = append(suggestedFixes, analysis.SuggestedFix{
94+
Message: fmt.Sprintf("replace %s json tag with %s", tagInfo.Name, tagReplacementName),
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+
}
112+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package notimestamp_test
18+
19+
import (
20+
"testing"
21+
22+
"golang.org/x/tools/go/analysis/analysistest"
23+
"sigs.k8s.io/kube-api-linter/pkg/analysis/notimestamp"
24+
)
25+
26+
func Test(t *testing.T) {
27+
testdata := analysistest.TestData()
28+
analysistest.RunWithSuggestedFixes(t, testdata, notimestamp.Analyzer, "a")
29+
}

pkg/analysis/notimestamp/doc.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
/*
18+
notimestamp provides a linter to ensure that structs do not contain a TimeStamp field.
19+
20+
The linter will flag any struct field containing the substring 'timestamp'. This means both
21+
TimeStamp and FooTimeStamp will be flagged.
22+
*/
23+
24+
package notimestamp
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package notimestamp
18+
19+
import (
20+
"sigs.k8s.io/kube-api-linter/pkg/analysis/initializer"
21+
"sigs.k8s.io/kube-api-linter/pkg/analysis/registry"
22+
)
23+
24+
func init() {
25+
registry.DefaultRegistry().RegisterLinter(Initializer())
26+
}
27+
28+
// Initializer returns the AnalyzerInitializer for this
29+
// Analyzer so that it can be added to the registry.
30+
func Initializer() initializer.AnalyzerInitializer {
31+
return initializer.NewInitializer(
32+
name,
33+
Analyzer,
34+
true,
35+
)
36+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package a
2+
3+
import (
4+
"time"
5+
6+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
7+
)
8+
9+
type NoTimeStampTestStruct struct {
10+
// +optional
11+
TimeStamp *time.Time `json:"timeStamp,omitempty"` // want "field TimeStamp: prefer use of the term time over timestamp"
12+
13+
// +optional
14+
Timestamp *time.Time `json:"timestamp,omitempty"` // want "field Timestamp: prefer use of the term time over timestamp"
15+
16+
// +optional
17+
FooTimeStamp *time.Time `json:"fooTimeStamp,omitempty"` // want "field FooTimeStamp: prefer use of the term time over timestamp"
18+
19+
// +optional
20+
FootimeStamp *time.Time `json:"footimeStamp,omitempty"` // want "field FootimeStamp: prefer use of the term time over timestamp"
21+
22+
// +optional
23+
BarTimestamp *time.Time `json:"barTimestamp,omitempty"` // want "field BarTimestamp: prefer use of the term time over timestamp"
24+
25+
// +optional
26+
FootimestampBar *time.Time `json:"fooTimestampBar,omitempty"` // want "field FootimestampBar: prefer use of the term time over timestamp"
27+
28+
// +optional
29+
FooTimestampBarTimeStamp *time.Time `json:"fooTimestampBarTimeStamp,omitempty"` // want "field FooTimestampBarTimeStamp: prefer use of the term time over timestamp"
30+
31+
// +optional
32+
MetaTimeStamp *metav1.Time `json:"metaTimeStamp,omitempty"` // want "field MetaTimeStamp: prefer use of the term time over timestamp"
33+
}
34+
35+
// DoNothing is used to check that the analyser doesn't report on methods.
36+
func (NoTimeStampTestStruct) DoNothing() {}
37+
38+
type NoSubTimeStampTestStruct struct {
39+
// +optional
40+
FooTimeStamp *time.Time `json:"fooTimeStamp,omitempty"` // want "field FooTimeStamp: prefer use of the term time over timestamp"
41+
}
42+
43+
type SerializedTimeStampTestStruct struct {
44+
// +optional
45+
FooTime *time.Time `json:"fooTime,omitempty"`
46+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package a
2+
3+
import (
4+
"time"
5+
6+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
7+
)
8+
9+
type NoTimeStampTestStruct struct {
10+
// +optional
11+
Time *time.Time `json:"time,omitempty"` // want "field TimeStamp: prefer use of the term time over timestamp"
12+
13+
// +optional
14+
Time *time.Time `json:"time,omitempty"` // want "field Timestamp: prefer use of the term time over timestamp"
15+
16+
// +optional
17+
FooTime *time.Time `json:"fooTime,omitempty"` // want "field FooTimeStamp: prefer use of the term time over timestamp"
18+
19+
// +optional
20+
FooTime *time.Time `json:"fooTime,omitempty"` // want "field FootimeStamp: prefer use of the term time over timestamp"
21+
22+
// +optional
23+
BarTime *time.Time `json:"barTime,omitempty"` // want "field BarTimestamp: prefer use of the term time over timestamp"
24+
25+
// +optional
26+
FooTimeBar *time.Time `json:"fooTimeBar,omitempty"` // want "field FootimestampBar: prefer use of the term time over timestamp"
27+
28+
// +optional
29+
FooTimeBarTime *time.Time `json:"fooTimeBarTime,omitempty"` // want "field FooTimestampBarTimeStamp: prefer use of the term time over timestamp"
30+
31+
// +optional
32+
MetaTime *metav1.Time `json:"metaTime,omitempty"` // want "field MetaTimeStamp: prefer use of the term time over timestamp"
33+
}
34+
35+
// DoNothing is used to check that the analyser doesn't report on methods.
36+
func (NoTimeStampTestStruct) DoNothing() {}
37+
38+
type NoSubTimeStampTestStruct struct {
39+
// +optional
40+
FooTime *time.Time `json:"fooTime,omitempty"` // want "field FooTimeStamp: prefer use of the term time over timestamp"
41+
}
42+
43+
type SerializedTimeStampTestStruct struct {
44+
// +optional
45+
FooTime *time.Time `json:"fooTime,omitempty"`
46+
}

0 commit comments

Comments
 (0)