Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
36 changes: 36 additions & 0 deletions docs/linters.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
- [Notimestamp](#notimestamp) - Prevents usage of 'TimeStamp' fields
- [OptionalFields](#optionalfields) - Validates optional field conventions
- [OptionalOrRequired](#optionalorrequired) - Ensures fields are explicitly marked as optional or required
- [References](#references) - Ensures field names use Ref/Refs instead of Reference/References
- [RequiredFields](#requiredfields) - Validates required field conventions
- [SSATags](#ssatags) - Ensures proper Server-Side Apply (SSA) tags on array fields
- [StatusOptional](#statusoptional) - Ensures status fields are marked as optional
Expand Down Expand Up @@ -555,6 +556,41 @@ If you prefer not to suggest fixes for pointers in required fields, you can chan
If you prefer not to suggest fixes for `omitempty` in required fields, you can change the `omitempty.policy` to `Warn` or `Ignore`.
If you prefer not to suggest fixes for `omitzero` in required fields, you can change the `omitzero.policy` to `Warn` and also not to consider `omitzero` policy at all, it can be set to `Forbid`.

## References

The `references` linter ensures that field names use 'Ref'/'Refs' instead of 'Reference'/'References'.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like we've established a pattern that for linters associated with a "don't do this" semantic we prefix with no.

I think staying consistent would be good, so I wonder if this should instead be noreference?

@JoelSpeed what are your thoughts here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think that makes sense, noreference or noreferences

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only pushback I can think of on that, is that the others are forbidding the usage of certain types. Which means this one forbidding just the naming might be a bit different? Does it imply that you aren't allowed reference types vs fields with references in the name?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have nophase and notimestamp which are naming conventions


By default, `references` is enabled and operates in standard mode, allowing 'Ref'/'Refs' but prohibiting 'Reference'/'References' in field names.

### Configuration

```yaml
lintersConfig:
references:
policy: AllowRefAndRefs | ForbidRefAndRefs # Defaults to `AllowRefAndRefs`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably more of a nitpick, but I wonder if something like PreferAbbreviatedReference and NoReference might be a bit more intuitive naming from the end-user perspective?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming suggestion works for me

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I think @JoelSpeed i agreed on something like PreferAbbreviatedReference and NoReferences

```

**Default behavior (policy: AllowRefAndRefs):**
- Reports errors for fields containing 'Reference' or 'References' and suggests replacing with 'Ref' or 'Refs'
- **Allows** fields containing 'Ref' or 'Refs' without reporting errors

**Strict mode (policy: ForbidRefAndRefs):**
- Reports errors for fields containing 'Reference' or 'References' anywhere and suggests replacing with 'Ref' or 'Refs'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This behaviour feels wrong still, suggesting to replace something that will then flag again?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1, this should be dropping of the offending text.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JoelSpeed @everettraven if y'all agree to dropping Ref/s too as offending text in ForbidRefAndRefs, can certainly make that change 👍🏻

- **Also reports errors** for fields containing 'Ref' or 'Refs' anywhere (no automatic fix provided)
- In this strict mode, the goal is to avoid all Ref/Refs/Reference/References in field names entirely

### Fixes

The `references` linter can automatically fix field names by replacing 'Reference' with 'Ref' and 'References' with 'Refs' anywhere in the field name.

For example:
- `NodeReference` → `NodeRef`
- `ReferenceNode` → `RefNode`
- `NodeReferenceConfig` → `NodeRefConfig`
- `NodeReferences` → `NodeRefs`

Note: In the `ForbidRefAndRefs` mode, the fixes are intermediate. As, the linter will still report errors on the resulting 'Ref'/'Refs' patterns, indicating they should be removed entirely
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can avoid this


## SSATags

The `ssatags` linter ensures that array fields in Kubernetes API objects have the appropriate
Expand Down
110 changes: 110 additions & 0 deletions pkg/analysis/references/analyzer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
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 references

import (
"errors"
"fmt"

"golang.org/x/tools/go/analysis"
"k8s.io/apimachinery/pkg/util/validation/field"
"sigs.k8s.io/kube-api-linter/pkg/analysis/initializer"
"sigs.k8s.io/kube-api-linter/pkg/analysis/namingconventions"
)

const (
name = "references"
doc = "Enforces that fields use Ref/Refs and not Reference/References"
)

var errUnexpectedInitializerType = errors.New("expected namingconventions.Initializer() to be of type initializer.ConfigurableAnalyzerInitializer, but was not")

// newAnalyzer creates a new analyzer for the references linter that is a wrapper around the namingconventions linter.
func newAnalyzer(cfg *Config) *analysis.Analyzer {
if cfg == nil {
cfg = &Config{}
}

// Default to AllowRefAndRefs if no policy is specified
policy := cfg.Policy
if policy == "" {
policy = PolicyAllowRefAndRefs
}

// Build the namingconventions config based on the policy
ncConfig := &namingconventions.Config{
Conventions: buildConventions(policy),
}

// Get the configurable initializer for namingconventions
configInit, ok := namingconventions.Initializer().(initializer.ConfigurableAnalyzerInitializer)
if !ok {
panic(fmt.Errorf("getting initializer: %w", errUnexpectedInitializerType))
}

// Validate generated namingconventions configuration
errs := configInit.ValidateConfig(ncConfig, field.NewPath("references"))
if err := errs.ToAggregate(); err != nil {
panic(fmt.Errorf("references linter has an invalid namingconventions configuration: %w", err))
}

// Initialize the wrapped analyzer
analyzer, err := configInit.Init(ncConfig)
if err != nil {
panic(fmt.Errorf("references linter encountered an error initializing wrapped namingconventions analyzer: %w", err))
}

analyzer.Name = name
analyzer.Doc = doc

return analyzer
}

// buildConventions creates the naming conventions based on the policy.
func buildConventions(policy Policy) []namingconventions.Convention {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try making this a switch on the policy, and returning a single convention from each branch of the switch, I think that will make the code more logical and the UX better

// Base convention: Replace "Reference" or "References" with "Ref" or "Refs"
// Using a single regex with optional 's' capture group to handle both cases
conventions := []namingconventions.Convention{
{
Name: "reference-to-ref",
ViolationMatcher: "(?i)reference(s?)",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we make this case insensitive? Isn't that causing things like Preference to become Prefs, I don't think that's actually desired? I think we are better not having the case insensitivity aren't we?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. That's a good question - I can't remember if removing the case sensitivity would mean we don't update the serialized form of the name correctly or not.

Would be good to double check that. If it still handles serialized name updates correctly, I agree we can put this back to being case sensitive.

Doing that means we do miss cases where the field name casing is incorrect though like Podreferences.

I wonder if adding a linter specifically for field name being PascalCase would make sense and consider something like Podreferences to be out of scope here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 @JoelSpeed i agree on making it case sensitive. Currently we are getting false positives as its converting Preference to Prefs which is documented as a known limitation.
@everettraven Circling back to our last discussion on PascalCase i still do feel that adding another linter for field name being PascalCase would make most sense.

We can possibly flag instances likePodreferences, referenceCount here to say field has invalid casing.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@krishagarwal278 Could you verify that removing the case insensitivity results in successfully matching the json tags that look like reference* and *Reference?

Operation: namingconventions.OperationReplacement,
Replacement: "Ref$1",
Message: "field names should use 'Ref' instead of 'Reference'",
},
}

// If policy is ForbidRefAndRefs, also flag Ref/Refs as problematic (no fix provided)
// This creates a two-step hint: fix Reference→Ref, but know that Ref also needs changing
if policy == PolicyForbidRefAndRefs {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it might make sense rather to append, to look into replace the list in this case (probably switch on the valid values?) and then look at how you could regex Reference Ref References and Refs` in a single regex for the drop

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Appending into a single regex for this makes so much sense!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, a single regex makes sense here.

conventions = append(conventions,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this wants to replace, not append. That way we can avoid the intermediate steps.

Should be able to just have a single regex here I think

namingconventions.Convention{
Name: "forbid-refs",
ViolationMatcher: "(?i)refs([^a-z]|$)",
Operation: namingconventions.OperationInform,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be a Drop rather than inform?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should just inform here because user wants to flag / forbidRefAndRefs they may not necessarily want to drop that substring itself.
For eg: We might not want NodeRef --> Node

dropping/swapping out ref(s) could be in scope for another story 👍🏻

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack, happy to have this as inform for now, will see if we get any feedback about it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I agree here. I think intention behind forbidRefAndRefs is actually to cause something like NodeRef -> Node.

If we think there is a reasonable use case to inform instead of suggest fixes, maybe we follow configuration precedence of other linters to allow a Warn and SuggestFixes configuration for this linter and we default to SuggestFixes?

Message: "should not use 'Refs'",
},
namingconventions.Convention{
Name: "forbid-ref",
ViolationMatcher: "(?i)ref([^ers]|$)",
Operation: namingconventions.OperationInform,
Message: "should not use 'Ref'",
},
)
}

return conventions
}
69 changes: 69 additions & 0 deletions pkg/analysis/references/analyzer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
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 references_test

import (
"testing"

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

func TestAllowRefAndRefs(t *testing.T) {
testdata := analysistest.TestData()

cfg := &references.Config{
Policy: references.PolicyAllowRefAndRefs,
}

analyzer, err := references.Initializer().Init(cfg)
if err != nil {
t.Fatalf("initializing references linter: %v", err)
}

analysistest.RunWithSuggestedFixes(t, testdata, analyzer, "a")
}

func TestEmptyConfig(t *testing.T) {
testdata := analysistest.TestData()

// Test with empty config - should default to AllowRefAndRefs behavior
cfg := &references.Config{}

analyzer, err := references.Initializer().Init(cfg)
if err != nil {
t.Fatalf("initializing references linter: %v", err)
}

// With default config (empty Policy), it should default to AllowRefAndRefs behavior
// So we test with folder 'a' which has the same expectations
analysistest.RunWithSuggestedFixes(t, testdata, analyzer, "a")
}

func TestForbidRefAndRefs(t *testing.T) {
testdata := analysistest.TestData()

cfg := &references.Config{
Policy: references.PolicyForbidRefAndRefs,
}

analyzer, err := references.Initializer().Init(cfg)
if err != nil {
t.Fatalf("initializing references linter: %v", err)
}

analysistest.RunWithSuggestedFixes(t, testdata, analyzer, "b")
}
34 changes: 34 additions & 0 deletions pkg/analysis/references/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
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 references

// policy defines the policy for handling Ref/Refs.
type Policy string

const (
// PolicyAllowRefAndRefs allows Ref/Refs in field names.
PolicyAllowRefAndRefs Policy = "AllowRefAndRefs"
// PolicyForbidRefAndRefs forbids Ref/Refs in field names.
PolicyForbidRefAndRefs Policy = "ForbidRefAndRefs"
)

// Config represents the configuration for the references linter.
type Config struct {
// policy controls whether Ref/Refs are allowed or forbidden in field names.
// When set to AllowRefAndRefs (default), fields containing Ref/Refs are allowed.
// When set to ForbidRefAndRefs, fields containing Ref/Refs are flagged as errors.
Policy Policy `json:"policy,omitempty"`
}
43 changes: 43 additions & 0 deletions pkg/analysis/references/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
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 `references` linter ensures that field names use 'Ref'/'Refs' instead of 'Reference'/'References'.
By default, `references` is enabled and enforces this naming convention.
The linter checks that 'Reference' anywhere in field names (beginning, middle, or end) is replaced with 'Ref'.
Similarly, 'References' anywhere in field names is replaced with 'Refs'.

Example configuration:
**Default behavior (allow Ref/Refs in field names):**
```yaml
lintersConfig:

references:
policy: AllowRefAndRefs

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIt, probably best to remove the blank lines in these code blocks

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not yet actioned

```
**Strict mode (forbid Ref/Refs in field names):**
```yaml
lintersConfig:

references:
policy: ForbidRefAndRefs

```
When `policy` is set to `AllowRefAndRefs` (the default), fields containing 'Ref' or 'Refs' are allowed.
The policy can be set to `ForbidRefAndRefs` to also report errors for 'Ref' or 'Refs' in field names.
*/
package references
63 changes: 63 additions & 0 deletions pkg/analysis/references/initializer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
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 references

import (
"golang.org/x/tools/go/analysis"
"k8s.io/apimachinery/pkg/util/validation/field"
"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.NewConfigurableInitializer(
name,
initAnalyzer,
true,
validateConfig,
)
}

func initAnalyzer(cfg *Config) (*analysis.Analyzer, error) {
return newAnalyzer(cfg), nil
}

// validateConfig validates the configuration for the references linter.
func validateConfig(cfg *Config, fldPath *field.Path) field.ErrorList {
if cfg == nil {
return nil // nil config is valid, will use defaults
}

var errs field.ErrorList

// Validate Policy enum if provided
switch cfg.Policy {
case PolicyAllowRefAndRefs, PolicyForbidRefAndRefs, "":
default:
errs = append(errs, field.NotSupported(
fldPath.Child("policy"),
cfg.Policy,
[]string{string(PolicyAllowRefAndRefs), string(PolicyForbidRefAndRefs)},
))
}
return errs
}
Loading