Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/linters.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- [JSONTags](#jsontags) - Ensures proper JSON tag formatting
- [MaxLength](#maxlength) - Checks for maximum length constraints on strings and arrays
- [NoBools](#nobools) - Prevents usage of boolean types
- [NoDurations](#nodurations) - Prevents usage of duration types
- [NoFloats](#nofloats) - Prevents usage of floating-point types
- [Nomaps](#nomaps) - Restricts usage of map types
- [NoNullable](#nonullable) - Prevents usage of the nullable marker
Expand Down Expand Up @@ -308,6 +309,14 @@ The `nobools` linter checks that fields in the API types do not contain a `bool`
Booleans are limited and do not evolve well over time.
It is recommended instead to create a string alias with meaningful values, as an enum.

## NoDurations

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.

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.

Instead, use an integer based field with a unit in the name, e.g. `FooSeconds`.

## NoFloats

The `nofloats` linter checks that fields in the API types do not contain a `float32` or `float64` type.
Expand Down
127 changes: 127 additions & 0 deletions pkg/analysis/nodurations/analyzer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
Copyright 2025 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package nodurations

import (
"fmt"
"go/ast"

"golang.org/x/tools/go/analysis"
kalerrors "sigs.k8s.io/kube-api-linter/pkg/analysis/errors"
"sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/extractjsontags"
"sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/inspector"
"sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/markers"
"sigs.k8s.io/kube-api-linter/pkg/analysis/utils"
)

const name = "nodurations"

// Analyzer is the analyzer for the nodurations package.
// It checks that no struct field is of a type either time.Duration or metav1.Duration.
var Analyzer = &analysis.Analyzer{
Name: name,
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.",
Run: run,
Requires: []*analysis.Analyzer{inspector.Analyzer},
}

func run(pass *analysis.Pass) (any, error) {
inspect, ok := pass.ResultOf[inspector.Analyzer].(inspector.Inspector)
if !ok {
return nil, kalerrors.ErrCouldNotGetInspector
}

inspect.InspectFields(func(field *ast.Field, _ []ast.Node, _ extractjsontags.FieldTagInfo, markersAccess markers.Markers) {
checkField(pass, field)
})

inspect.InspectTypeSpec(func(typeSpec *ast.TypeSpec, markersAccess markers.Markers) {
checkTypeSpec(pass, typeSpec, typeSpec, "type")
})

return nil, nil //nolint:nilnil
}

func checkField(pass *analysis.Pass, field *ast.Field) {
fieldName := utils.FieldName(field)
if fieldName == "" {
return
}

prefix := fmt.Sprintf("field %s", fieldName)

checkTypeExpr(pass, field.Type, field, prefix)
}

//nolint:cyclop
func checkTypeExpr(pass *analysis.Pass, typeExpr ast.Expr, node ast.Node, prefix string) {
switch typ := typeExpr.(type) {
case *ast.SelectorExpr:
pkg, ok := typ.X.(*ast.Ident)
if !ok {
return
}

if typ.X == nil || (pkg.Name != "time" && pkg.Name != "metav1") {
return
}

// Array element is not a metav1.Condition.
if typ.Sel == nil || typ.Sel.Name != "Duration" {
return
}

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)
case *ast.Ident:
checkIdent(pass, typ, node, prefix)
case *ast.StarExpr:
checkTypeExpr(pass, typ.X, node, fmt.Sprintf("%s pointer", prefix))
case *ast.ArrayType:
checkTypeExpr(pass, typ.Elt, node, fmt.Sprintf("%s array element", prefix))
case *ast.MapType:
checkTypeExpr(pass, typ.Key, node, fmt.Sprintf("%s map key", prefix))
checkTypeExpr(pass, typ.Value, node, fmt.Sprintf("%s map value", prefix))
}
}

// checkIdent calls the checkFunc with the ident, when we have hit a built-in type.
// If the ident is not a built in, we look at the underlying type until we hit a built-in type.
func checkIdent(pass *analysis.Pass, ident *ast.Ident, node ast.Node, prefix string) {
if utils.IsBasicType(pass, ident) {
// We've hit a built-in type, no need to check further.
return
}

tSpec, ok := utils.LookupTypeSpec(pass, ident)
if !ok {
return
}

// The field is using a type alias, check if the alias is an int.
checkTypeSpec(pass, tSpec, node, fmt.Sprintf("%s type", prefix))
}

func checkTypeSpec(pass *analysis.Pass, tSpec *ast.TypeSpec, node ast.Node, prefix string) {
if tSpec.Name == nil {
return
}

typeName := tSpec.Name.Name
prefix = fmt.Sprintf("%s %s", prefix, typeName)

checkTypeExpr(pass, tSpec.Type, node, prefix)
}
29 changes: 29 additions & 0 deletions pkg/analysis/nodurations/analyzer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
Copyright 2025 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package nodurations_test

import (
"testing"

"golang.org/x/tools/go/analysis/analysistest"
"sigs.k8s.io/kube-api-linter/pkg/analysis/nodurations"
)

func Test(t *testing.T) {
testdata := analysistest.TestData()
analysistest.Run(t, testdata, nodurations.Analyzer, "a")
}
25 changes: 25 additions & 0 deletions pkg/analysis/nodurations/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
Copyright 2025 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

/*
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.

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.

Instead, use an integer based field with a unit in the name, e.g. `FooSeconds`.
*/

package nodurations
36 changes: 36 additions & 0 deletions pkg/analysis/nodurations/initializer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
Copyright 2025 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package nodurations

import (
"sigs.k8s.io/kube-api-linter/pkg/analysis/initializer"
"sigs.k8s.io/kube-api-linter/pkg/analysis/registry"
)

func init() {
registry.DefaultRegistry().RegisterLinter(Initializer())
}

// Initializer returns the AnalyzerInitializer for this
// Analyzer so that it can be added to the registry.
func Initializer() initializer.AnalyzerInitializer {
return initializer.NewInitializer(
name,
Analyzer,
true,
)
}
110 changes: 110 additions & 0 deletions pkg/analysis/nodurations/testdata/src/a/a.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package a

import (
"time"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

type Durations struct {
ValidString string

ValidMap map[string]string

ValidInt32 int32

ValidInt64 int64

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."

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."

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."

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."

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."

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."

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."

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."

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."

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."

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."

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."

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."

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."
}

// DoNothing is used to check that the analyser doesn't report on methods.
func (Durations) DoNothing(a bool) bool {
return a
}

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."

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."

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."

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."

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."

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."

type DurationsWithMetaV1Package struct {
ValidString string

ValidMap map[string]string

ValidInt32 int32

ValidInt64 int64

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."

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."

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."

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."

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."

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."

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."

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."

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."

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."

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."

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."

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."
}

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."

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."

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."

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."

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."

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."
15 changes: 15 additions & 0 deletions pkg/analysis/nodurations/testdata/src/a/b.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package a

import (
"time"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

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."

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."

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."

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."
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
This is a copy of the minimum amount of the original file to be able to test the nodurations linter.
*/

package v1

import "time"

// Duration is a wrapper around time.Duration which supports correct
// marshaling to YAML and JSON. In particular, it marshals into strings, which
// can be used as map keys in json.
type Duration struct {
time.Duration `protobuf:"varint,1,opt,name=duration,casttype=time.Duration"`
}
Loading