Skip to content

Commit 65a570b

Browse files
authored
Merge pull request #143 from Karthik-K-N/nodurations
Add nodurations linter
2 parents 81d935c + af04de5 commit 65a570b

File tree

9 files changed

+366
-0
lines changed

9 files changed

+366
-0
lines changed

docs/linters.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
- [JSONTags](#jsontags) - Ensures proper JSON tag formatting
1010
- [MaxLength](#maxlength) - Checks for maximum length constraints on strings and arrays
1111
- [NoBools](#nobools) - Prevents usage of boolean types
12+
- [NoDurations](#nodurations) - Prevents usage of duration types
1213
- [NoFloats](#nofloats) - Prevents usage of floating-point types
1314
- [Nomaps](#nomaps) - Restricts usage of map types
1415
- [NoNullable](#nonullable) - Prevents usage of the nullable marker
@@ -308,6 +309,14 @@ The `nobools` linter checks that fields in the API types do not contain a `bool`
308309
Booleans are limited and do not evolve well over time.
309310
It is recommended instead to create a string alias with meaningful values, as an enum.
310311

312+
## NoDurations
313+
314+
The `nodurations` linter checks that fields in the API types do not contain a `Duration` type ether from the `time` package or the `k8s.io/apimachinery/pkg/apis/meta/v1` package.
315+
316+
It is recommended to avoid the use of Duration types. Their use ties the API to Go's notion of duration parsing, which may be hard to implement in other languages.
317+
318+
Instead, use an integer based field with a unit in the name, e.g. `FooSeconds`.
319+
311320
## NoFloats
312321

313322
The `nofloats` linter checks that fields in the API types do not contain a `float32` or `float64` type.
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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 nodurations
18+
19+
import (
20+
"fmt"
21+
"go/ast"
22+
23+
"golang.org/x/tools/go/analysis"
24+
kalerrors "sigs.k8s.io/kube-api-linter/pkg/analysis/errors"
25+
"sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/extractjsontags"
26+
"sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/inspector"
27+
"sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/markers"
28+
"sigs.k8s.io/kube-api-linter/pkg/analysis/utils"
29+
)
30+
31+
const name = "nodurations"
32+
33+
// Analyzer is the analyzer for the nodurations package.
34+
// It checks that no struct field is of a type either time.Duration or metav1.Duration.
35+
var Analyzer = &analysis.Analyzer{
36+
Name: name,
37+
Doc: "Duration types should not be used, to avoid the need for clients to implement go duration parsing. Instead, use integer based fields with the unit in the field name.",
38+
Run: run,
39+
Requires: []*analysis.Analyzer{inspector.Analyzer},
40+
}
41+
42+
func run(pass *analysis.Pass) (any, error) {
43+
inspect, ok := pass.ResultOf[inspector.Analyzer].(inspector.Inspector)
44+
if !ok {
45+
return nil, kalerrors.ErrCouldNotGetInspector
46+
}
47+
48+
inspect.InspectFields(func(field *ast.Field, _ []ast.Node, _ extractjsontags.FieldTagInfo, markersAccess markers.Markers) {
49+
checkField(pass, field)
50+
})
51+
52+
inspect.InspectTypeSpec(func(typeSpec *ast.TypeSpec, markersAccess markers.Markers) {
53+
checkTypeSpec(pass, typeSpec, typeSpec, "type")
54+
})
55+
56+
return nil, nil //nolint:nilnil
57+
}
58+
59+
func checkField(pass *analysis.Pass, field *ast.Field) {
60+
fieldName := utils.FieldName(field)
61+
if fieldName == "" {
62+
return
63+
}
64+
65+
prefix := fmt.Sprintf("field %s", fieldName)
66+
67+
checkTypeExpr(pass, field.Type, field, prefix)
68+
}
69+
70+
//nolint:cyclop
71+
func checkTypeExpr(pass *analysis.Pass, typeExpr ast.Expr, node ast.Node, prefix string) {
72+
switch typ := typeExpr.(type) {
73+
case *ast.SelectorExpr:
74+
pkg, ok := typ.X.(*ast.Ident)
75+
if !ok {
76+
return
77+
}
78+
79+
if typ.X == nil || (pkg.Name != "time" && pkg.Name != "metav1") {
80+
return
81+
}
82+
83+
// Array element is not a metav1.Condition.
84+
if typ.Sel == nil || typ.Sel.Name != "Duration" {
85+
return
86+
}
87+
88+
pass.Reportf(node.Pos(), "%s should not use a Duration. Use an integer type with units in the name to avoid the need for clients to implement Go style duration parsing.", prefix)
89+
case *ast.Ident:
90+
checkIdent(pass, typ, node, prefix)
91+
case *ast.StarExpr:
92+
checkTypeExpr(pass, typ.X, node, fmt.Sprintf("%s pointer", prefix))
93+
case *ast.ArrayType:
94+
checkTypeExpr(pass, typ.Elt, node, fmt.Sprintf("%s array element", prefix))
95+
case *ast.MapType:
96+
checkTypeExpr(pass, typ.Key, node, fmt.Sprintf("%s map key", prefix))
97+
checkTypeExpr(pass, typ.Value, node, fmt.Sprintf("%s map value", prefix))
98+
}
99+
}
100+
101+
// checkIdent calls the checkFunc with the ident, when we have hit a built-in type.
102+
// If the ident is not a built in, we look at the underlying type until we hit a built-in type.
103+
func checkIdent(pass *analysis.Pass, ident *ast.Ident, node ast.Node, prefix string) {
104+
if utils.IsBasicType(pass, ident) {
105+
// We've hit a built-in type, no need to check further.
106+
return
107+
}
108+
109+
tSpec, ok := utils.LookupTypeSpec(pass, ident)
110+
if !ok {
111+
return
112+
}
113+
114+
// The field is using a type alias, check if the alias is an int.
115+
checkTypeSpec(pass, tSpec, node, fmt.Sprintf("%s type", prefix))
116+
}
117+
118+
func checkTypeSpec(pass *analysis.Pass, tSpec *ast.TypeSpec, node ast.Node, prefix string) {
119+
if tSpec.Name == nil {
120+
return
121+
}
122+
123+
typeName := tSpec.Name.Name
124+
prefix = fmt.Sprintf("%s %s", prefix, typeName)
125+
126+
checkTypeExpr(pass, tSpec.Type, node, prefix)
127+
}
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 nodurations_test
18+
19+
import (
20+
"testing"
21+
22+
"golang.org/x/tools/go/analysis/analysistest"
23+
"sigs.k8s.io/kube-api-linter/pkg/analysis/nodurations"
24+
)
25+
26+
func Test(t *testing.T) {
27+
testdata := analysistest.TestData()
28+
analysistest.Run(t, testdata, nodurations.Analyzer, "a")
29+
}

pkg/analysis/nodurations/doc.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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+
The `nodurations` linter checks that fields in the API types do not contain `Duration` type ether from the `time` package or the `k8s.io/apimachinery/pkg/apis/meta/v1` package.
19+
20+
It is recommended to avoid the use of Duration types. Their use ties the API to Go's notion of duration parsing, which may be hard to implement in other languages.
21+
22+
Instead, use an integer based field with a unit in the name, e.g. `FooSeconds`.
23+
*/
24+
25+
package nodurations
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 nodurations
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: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package a
2+
3+
import (
4+
"time"
5+
6+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
7+
)
8+
9+
type Durations struct {
10+
ValidString string
11+
12+
ValidMap map[string]string
13+
14+
ValidInt32 int32
15+
16+
ValidInt64 int64
17+
18+
InvalidDuration time.Duration // want "field InvalidDuration should not use a Duration. Use an integer type with units in the name to avoid the need for clients to implement Go style duration parsing."
19+
20+
InvalidDurationPtr *time.Duration // want "field InvalidDurationPtr pointer should not use a Duration. Use an integer type with units in the name to avoid the need for clients to implement Go style duration parsing."
21+
22+
InvalidDurationSlice []time.Duration // want "field InvalidDurationSlice array element should not use a Duration. Use an integer type with units in the name to avoid the need for clients to implement Go style duration parsing."
23+
24+
InvalidDurationPtrSlice []*time.Duration // want "field InvalidDurationPtrSlice array element pointer should not use a Duration. Use an integer type with units in the name to avoid the need for clients to implement Go style duration parsing."
25+
26+
InvalidDurationAlias DurationAlias // want "field InvalidDurationAlias type DurationAlias should not use a Duration. Use an integer type with units in the name to avoid the need for clients to implement Go style duration parsing."
27+
28+
InvalidDurationPtrAlias *DurationAlias // want "field InvalidDurationPtrAlias pointer type DurationAlias should not use a Duration. Use an integer type with units in the name to avoid the need for clients to implement Go style duration parsing."
29+
30+
InvalidDurationSliceAlias []DurationAlias // want "field InvalidDurationSliceAlias array element type DurationAlias should not use a Duration. Use an integer type with units in the name to avoid the need for clients to implement Go style duration parsing."
31+
32+
InvalidDurationPtrSliceAlias []*DurationAlias // want "field InvalidDurationPtrSliceAlias array element pointer type DurationAlias should not use a Duration. Use an integer type with units in the name to avoid the need for clients to implement Go style duration parsing."
33+
34+
InvalidMapStringToDuration map[string]time.Duration // want "field InvalidMapStringToDuration map value should not use a Duration. Use an integer type with units in the name to avoid the need for clients to implement Go style duration parsing."
35+
36+
InvalidMapStringToDurationPtr map[string]*time.Duration // want "field InvalidMapStringToDurationPtr map value pointer should not use a Duration. Use an integer type with units in the name to avoid the need for clients to implement Go style duration parsing."
37+
38+
InvalidMapDurationToString map[time.Duration]string // want "field InvalidMapDurationToString map key should not use a Duration. Use an integer type with units in the name to avoid the need for clients to implement Go style duration parsing."
39+
40+
InvalidMapDurationPtrToString map[*time.Duration]string // want "field InvalidMapDurationPtrToString map key pointer should not use a Duration. Use an integer type with units in the name to avoid the need for clients to implement Go style duration parsing."
41+
42+
InvalidDurationAliasFromAnotherFile DurationAliasB // want "field InvalidDurationAliasFromAnotherFile type DurationAliasB should not use a Duration. Use an integer type with units in the name to avoid the need for clients to implement Go style duration parsing."
43+
44+
InvalidDurationPtrAliasFromAnotherFile *DurationAliasB // want "field InvalidDurationPtrAliasFromAnotherFile pointer type DurationAliasB should not use a Duration. Use an integer type with units in the name to avoid the need for clients to implement Go style duration parsing."
45+
}
46+
47+
// DoNothing is used to check that the analyser doesn't report on methods.
48+
func (Durations) DoNothing(a bool) bool {
49+
return a
50+
}
51+
52+
type DurationAlias time.Duration // want "type DurationAlias should not use a Duration. Use an integer type with units in the name to avoid the need for clients to implement Go style duration parsing."
53+
54+
type DurationAliasPtr *time.Duration // want "type DurationAliasPtr pointer should not use a Duration. Use an integer type with units in the name to avoid the need for clients to implement Go style duration parsing."
55+
56+
type DurationAliasSlice []time.Duration // want "type DurationAliasSlice array element should not use a Duration. Use an integer type with units in the name to avoid the need for clients to implement Go style duration parsing."
57+
58+
type DurationAliasPtrSlice []*time.Duration // want "type DurationAliasPtrSlice array element pointer should not use a Duration. Use an integer type with units in the name to avoid the need for clients to implement Go style duration parsing."
59+
60+
type MapStringToDurationAlias map[string]time.Duration // want "type MapStringToDurationAlias map value should not use a Duration. Use an integer type with units in the name to avoid the need for clients to implement Go style duration parsing."
61+
62+
type MapStringToDurationPtrAlias map[string]*time.Duration // want "type MapStringToDurationPtrAlias map value pointer should not use a Duration. Use an integer type with units in the name to avoid the need for clients to implement Go style duration parsing."
63+
64+
type DurationsWithMetaV1Package struct {
65+
ValidString string
66+
67+
ValidMap map[string]string
68+
69+
ValidInt32 int32
70+
71+
ValidInt64 int64
72+
73+
InvalidDuration metav1.Duration // want "field InvalidDuration should not use a Duration. Use an integer type with units in the name to avoid the need for clients to implement Go style duration parsing."
74+
75+
InvalidDurationPtr *metav1.Duration // want "field InvalidDurationPtr pointer should not use a Duration. Use an integer type with units in the name to avoid the need for clients to implement Go style duration parsing."
76+
77+
InvalidDurationSlice []metav1.Duration // want "field InvalidDurationSlice array element should not use a Duration. Use an integer type with units in the name to avoid the need for clients to implement Go style duration parsing."
78+
79+
InvalidDurationPtrSlice []*metav1.Duration // want "field InvalidDurationPtrSlice array element pointer should not use a Duration. Use an integer type with units in the name to avoid the need for clients to implement Go style duration parsing."
80+
81+
InvalidDurationAlias DurationAliasWithMetaV1 // want "field InvalidDurationAlias type DurationAliasWithMetaV1 should not use a Duration. Use an integer type with units in the name to avoid the need for clients to implement Go style duration parsing."
82+
83+
InvalidDurationPtrAlias *DurationAliasWithMetaV1 // want "field InvalidDurationPtrAlias pointer type DurationAliasWithMetaV1 should not use a Duration. Use an integer type with units in the name to avoid the need for clients to implement Go style duration parsing."
84+
85+
InvalidDurationSliceAlias []DurationAliasWithMetaV1 // want "field InvalidDurationSliceAlias array element type DurationAliasWithMetaV1 should not use a Duration. Use an integer type with units in the name to avoid the need for clients to implement Go style duration parsing."
86+
87+
InvalidDurationPtrSliceAlias []*DurationAliasWithMetaV1 // want "field InvalidDurationPtrSliceAlias array element pointer type DurationAliasWithMetaV1 should not use a Duration. Use an integer type with units in the name to avoid the need for clients to implement Go style duration parsing."
88+
89+
InvalidMapStringToDuration map[string]metav1.Duration // want "field InvalidMapStringToDuration map value should not use a Duration. Use an integer type with units in the name to avoid the need for clients to implement Go style duration parsing."
90+
91+
InvalidMapStringToDurationPtr map[string]*metav1.Duration // want "field InvalidMapStringToDurationPtr map value pointer should not use a Duration. Use an integer type with units in the name to avoid the need for clients to implement Go style duration parsing."
92+
93+
InvalidMapDurationToString map[metav1.Duration]string // want "field InvalidMapDurationToString map key should not use a Duration. Use an integer type with units in the name to avoid the need for clients to implement Go style duration parsing."
94+
95+
InvalidMapDurationPtrToString map[*metav1.Duration]string // want "field InvalidMapDurationPtrToString map key pointer should not use a Duration. Use an integer type with units in the name to avoid the need for clients to implement Go style duration parsing."
96+
97+
InvalidDurationAliasFromAnotherFile DurationAliasBWithMetaV1 // want "field InvalidDurationAliasFromAnotherFile type DurationAliasBWithMetaV1 should not use a Duration. Use an integer type with units in the name to avoid the need for clients to implement Go style duration parsing."
98+
}
99+
100+
type DurationAliasWithMetaV1 metav1.Duration // want "type DurationAliasWithMetaV1 should not use a Duration. Use an integer type with units in the name to avoid the need for clients to implement Go style duration parsing."
101+
102+
type DurationAliasPtrWithMetaV1 *metav1.Duration // want "type DurationAliasPtrWithMetaV1 pointer should not use a Duration. Use an integer type with units in the name to avoid the need for clients to implement Go style duration parsing."
103+
104+
type DurationAliasSliceWithMetaV1 []metav1.Duration // want "type DurationAliasSliceWithMetaV1 array element should not use a Duration. Use an integer type with units in the name to avoid the need for clients to implement Go style duration parsing."
105+
106+
type DurationAliasPtrSliceWithMetaV1 []*metav1.Duration // want "type DurationAliasPtrSliceWithMetaV1 array element pointer should not use a Duration. Use an integer type with units in the name to avoid the need for clients to implement Go style duration parsing."
107+
108+
type MapStringToDurationAliaWithMetaV1 map[string]metav1.Duration // want "type MapStringToDurationAliaWithMetaV1 map value should not use a Duration. Use an integer type with units in the name to avoid the need for clients to implement Go style duration parsing."
109+
110+
type MapStringToDurationPtrAliasWithMetaV1 map[string]*metav1.Duration // want "type MapStringToDurationPtrAliasWithMetaV1 map value pointer should not use a Duration. Use an integer type with units in the name to avoid the need for clients to implement Go style duration parsing."
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package a
2+
3+
import (
4+
"time"
5+
6+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
7+
)
8+
9+
type DurationAliasB time.Duration // want "type DurationAliasB should not use a Duration. Use an integer type with units in the name to avoid the need for clients to implement Go style duration parsing."
10+
11+
type DurationAliasPtrB *time.Duration // want "type DurationAliasPtrB pointer should not use a Duration. Use an integer type with units in the name to avoid the need for clients to implement Go style duration parsing."
12+
13+
type DurationAliasBWithMetaV1 metav1.Duration // want "type DurationAliasBWithMetaV1 should not use a Duration. Use an integer type with units in the name to avoid the need for clients to implement Go style duration parsing."
14+
15+
type DurationAliasPtrBWithMetaV1 *metav1.Duration // want "type DurationAliasPtrBWithMetaV1 pointer should not use a Duration. Use an integer type with units in the name to avoid the need for clients to implement Go style duration parsing."
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*
2+
This is a copy of the minimum amount of the original file to be able to test the nodurations linter.
3+
*/
4+
5+
package v1
6+
7+
import "time"
8+
9+
// Duration is a wrapper around time.Duration which supports correct
10+
// marshaling to YAML and JSON. In particular, it marshals into strings, which
11+
// can be used as map keys in json.
12+
type Duration struct {
13+
time.Duration `protobuf:"varint,1,opt,name=duration,casttype=time.Duration"`
14+
}

0 commit comments

Comments
 (0)