Skip to content

Commit 257922b

Browse files
committed
Add notimestamp linter
1 parent e719da1 commit 257922b

File tree

8 files changed

+227
-0
lines changed

8 files changed

+227
-0
lines changed

docs/linters.md

Lines changed: 5 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
@@ -151,6 +152,10 @@ The `nomaps` linter checks the usage of map types.
151152

152153
Maps are discouraged apart from `map[string]string` which is used for labels and annotations in Kubernetes APIs since it's hard to distinguish between structs and maps in spec. Instead of plain map, lists of named subobjects are preferred.
153154

155+
## Notimestamp
156+
157+
The `notimestamp` linter checks that the fields in the API types don't contain a 'Timestamp', or any field which contains 'Timestamp' as a substring, e.g CreateTimestamp.
158+
154159
### Configuration
155160

156161
```yaml

pkg/analysis/notimestamp/analyzer.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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+
"go/ast"
21+
"strings"
22+
23+
"golang.org/x/tools/go/analysis"
24+
"golang.org/x/tools/go/analysis/passes/inspect"
25+
"golang.org/x/tools/go/ast/inspector"
26+
kalerrors "sigs.k8s.io/kube-api-linter/pkg/analysis/errors"
27+
"sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/extractjsontags"
28+
"sigs.k8s.io/kube-api-linter/pkg/analysis/utils"
29+
)
30+
31+
const name = "notimestamp"
32+
33+
// Analyzer is the analyzer for the notimestamp package.
34+
// It checks that no struct fields named 'timestamp', or that contain timestamp as a
35+
// substring are present.
36+
var Analyzer = &analysis.Analyzer{
37+
Name: name,
38+
Doc: "check that non of the struct field named timestamp or contain timestamp as a substring",
39+
Run: run,
40+
Requires: []*analysis.Analyzer{inspect.Analyzer, extractjsontags.Analyzer},
41+
}
42+
43+
func run(pass *analysis.Pass) (any, error) {
44+
inspect, ok := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
45+
if !ok {
46+
return nil, kalerrors.ErrCouldNotGetInspector
47+
}
48+
49+
jsonTags, ok := pass.ResultOf[extractjsontags.Analyzer].(extractjsontags.StructFieldTags)
50+
if !ok {
51+
return nil, kalerrors.ErrCouldNotGetJSONTags
52+
}
53+
54+
// Filter to fields so that we can iterate over fields in a struct.
55+
nodeFilter := []ast.Node{
56+
(*ast.Field)(nil),
57+
}
58+
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+
})
95+
96+
return nil, nil //nolint:nilnil
97+
}
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+
"sigs.k8s.io/kube-api-linter/pkg/analysis/notimestamp"
21+
"testing"
22+
23+
"golang.org/x/tools/go/analysis/analysistest"
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: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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+
"golang.org/x/tools/go/analysis"
21+
"sigs.k8s.io/kube-api-linter/pkg/config"
22+
)
23+
24+
// Initializer returns the AnalyzerInitializer for this
25+
// Analyzer so that it can be added to the registry.
26+
func Initializer() initializer {
27+
return initializer{}
28+
}
29+
30+
// intializer implements the AnalyzerInitializer interface.
31+
type initializer struct{}
32+
33+
// Name returns the name of the Analyzer.
34+
func (initializer) Name() string {
35+
return name
36+
}
37+
38+
// Init returns the intialized Analyzer.
39+
func (initializer) Init(cfg config.LintersConfig) (*analysis.Analyzer, error) {
40+
return Analyzer, nil
41+
}
42+
43+
// Default determines whether this Analyzer is on by default, or not.
44+
func (initializer) Default() bool {
45+
return true
46+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package a
2+
3+
import "time"
4+
5+
type NoTimeStampTestStruct struct {
6+
// +optional
7+
TimeStamp *time.Time `json:"timeStamp,omitempty"` // want "field TimeStamp: fields with timestamp substring should be avoided"
8+
}
9+
10+
// DoNothing is used to check that the analyser doesn't report on methods.
11+
func (NoTimeStampTestStruct) DoNothing() {}
12+
13+
type NoSubTimeStampTestStruct struct {
14+
// +optional
15+
FooTimeStamp *time.Time `json:"fooTimeStamp,omitempty"` // want "field FooTimeStamp: fields with timestamp substring should be avoided"
16+
17+
}
18+
19+
type SerializedTimeStampTestStruct struct {
20+
// +optional
21+
FooTime *time.Time `json:"fooTime,omitempty"`
22+
}

pkg/analysis/registry.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"sigs.k8s.io/kube-api-linter/pkg/analysis/nofloats"
3030
"sigs.k8s.io/kube-api-linter/pkg/analysis/nomaps"
3131
"sigs.k8s.io/kube-api-linter/pkg/analysis/nophase"
32+
"sigs.k8s.io/kube-api-linter/pkg/analysis/notimestamp"
3233
"sigs.k8s.io/kube-api-linter/pkg/analysis/optionalfields"
3334
"sigs.k8s.io/kube-api-linter/pkg/analysis/optionalorrequired"
3435
"sigs.k8s.io/kube-api-linter/pkg/analysis/requiredfields"
@@ -86,6 +87,7 @@ func NewRegistry() Registry {
8687
nofloats.Initializer(),
8788
nomaps.Initializer(),
8889
nophase.Initializer(),
90+
notimestamp.Initializer(),
8991
optionalfields.Initializer(),
9092
optionalorrequired.Initializer(),
9193
requiredfields.Initializer(),

pkg/analysis/registry_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ var _ = Describe("Registry", func() {
3939
"nofloats",
4040
"nomaps",
4141
"nophase",
42+
"notimestamp",
4243
"optionalfields",
4344
"optionalorrequired",
4445
"requiredfields",
@@ -62,6 +63,7 @@ var _ = Describe("Registry", func() {
6263
"nofloats",
6364
"nomaps",
6465
"nophase",
66+
"notimestamp",
6567
"optionalfields",
6668
"optionalorrequired",
6769
"requiredfields",

0 commit comments

Comments
 (0)