Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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 strict mode, prohibiting the use of 'Reference', 'References', 'Ref', or 'Refs' anywhere in field names
Copy link
Contributor

Choose a reason for hiding this comment

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

Default should be to allow Ref/Refs, but not Reference/References


### Configuration

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

**Default behavior (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.

Why would it suggest to replace with something that it will then report is bad?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yikes, i guess I misunderstood the default policy to be forbidRefAndRefs. It's now allowRefAndRefs which makes sense because we want to allow Ref and Refs but not Reference and References as the default configuration option

- **Also reports errors** for fields containing 'Ref' or 'Refs' anywhere (these should be dropped/removed entirely)
- In this strict mode, the goal is to avoid all Ref/Refs/Reference/References in field names

**Allow Ref/Refs (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

### 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 default `ForbidRefAndRefs` mode, these fixes are intermediate - the linter will still report errors on the resulting 'Ref'/'Refs' patterns, indicating they should be removed entirely

## SSATags

The `ssatags` linter ensures that array fields in Kubernetes API objects have the appropriate
Expand Down
6 changes: 4 additions & 2 deletions pkg/analysis/namingconventions/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,11 @@ type analyzer struct {
conventions []Convention
}

// newAnalyzer creates a new analysis.Analyzer for the namingconventions
// NewAnalyzer creates a new analysis.Analyzer for the namingconventions
// linter based on the provided config.
func newAnalyzer(cfg *Config) *analysis.Analyzer {
// This function is exported to allow other linters to wrap namingconventions
// with fixed configuration.
func NewAnalyzer(cfg *Config) *analysis.Analyzer {
Copy link
Contributor

Choose a reason for hiding this comment

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

Are we certain this needs to be changed to exported? I thought we avoided that on the other PR

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ahh nice catch. Guess we can avoid it here too!

a := &analyzer{
conventions: cfg.Conventions,
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/analysis/namingconventions/initializer.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func Initializer() initializer.AnalyzerInitializer {
}

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

// validateConfig implements validation of the conditions linter config.
Expand Down
105 changes: 105 additions & 0 deletions pkg/analysis/references/analyzer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
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"
"sigs.k8s.io/kube-api-linter/pkg/analysis/namingconventions"
)

const name = "references"

// newAnalyzer creates a new analysis.Analyzer for the references linter.
// The references linter is implemented as a wrapper around the namingconventions
// linter with fixed configuration based on the policy.
func newAnalyzer(cfg *Config) *analysis.Analyzer {
if cfg == nil {
cfg = &Config{}
}

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

// Build naming conventions based on policy
conventions := buildConventions(policy)

// Create namingconventions config
ncConfig := &namingconventions.Config{
Conventions: conventions,
}

// Create the underlying namingconventions analyzer
ncAnalyzer := namingconventions.NewAnalyzer(ncConfig)

// Wrap it to return our name
analyzer := &analysis.Analyzer{
Name: name,
Doc: "Enforces that fields use Ref/Refs and not Reference/References",
Run: ncAnalyzer.Run,
Requires: ncAnalyzer.Requires,
}

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

conventions := []namingconventions.Convention{
// Match "References" anywhere (case-sensitive, capital R)
{
Name: "references-to-refs",
ViolationMatcher: "References",
Operation: namingconventions.OperationReplacement,
Replacement: "Refs",
Message: "field names should use 'Refs' instead of 'References'",
},
// Match "Reference" but not when part of "References"
// Match Reference followed by: end-of-string OR non-'s' character
{
Name: "reference-to-ref",
ViolationMatcher: "Reference([^s]|$)",
Operation: namingconventions.OperationReplacement,
Replacement: "Ref$1",
Message: "field names should use 'Ref' instead of 'Reference'",
},
}

// If policy is ForbidRefAndRefs, add conventions to forbid Ref/Refs anywhere
// Exclude patterns already handled by Reference/References above
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

// Match "Refs" but not when part of "References"
namingconventions.Convention{
Name: "forbid-refs",
ViolationMatcher: "Refs([^a-z]|$)",
Copy link
Contributor

Choose a reason for hiding this comment

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

Refs would not be part of references? I don't think the extra capture is needed here is it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this was part of forbidRefAndRefs policy

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'",
},
// Match "Ref" but not when part of "Reference", "References", or "Refs"
namingconventions.Convention{
Name: "forbid-ref",
ViolationMatcher: "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 ForbidRefAndRefs 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 ForbidRefAndRefs behavior
// So we test with folder 'b' which has the same expectations
analysistest.RunWithSuggestedFixes(t, testdata, analyzer, "b")
}

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.
Copy link
Contributor

Choose a reason for hiding this comment

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

For consistency with other rules

Suggested change
// Policy controls whether Ref/Refs are allowed or forbidden in field names.
// policy controls whether Ref/Refs are allowed or forbidden in field names.

// When set to AllowRefAndRefs, fields containing Ref/Refs are allowed.
// When set to ForbidRefAndRefs (default), fields containing Ref/Refs are flagged as errors.
Policy Policy `json:"policy,omitempty"`
}
48 changes: 48 additions & 0 deletions pkg/analysis/references/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
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 (forbid Ref/Refs in field names):**
```yaml
lintersConfig:

references:
policy: ForbidRefAndRefs

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

```

**For compatibility (allow Ref/Refs in field names):**
```yaml
lintersConfig:

references:
policy: AllowRefAndRefs

```

When `policy` is set to `ForbidRefAndRefs` (the default), fields containing 'Ref' or 'Refs' anywhere in their names will be reported as errors.
This is useful to ensure consistency across the codebase. The policy can be set to `AllowRefAndRefs` to allow such field names.
*/
package references
62 changes: 62 additions & 0 deletions pkg/analysis/references/initializer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
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
if cfg.Policy != "" && cfg.Policy != PolicyAllowRefAndRefs && cfg.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.

Better to use a switch here and have the default case error

errs = append(errs, field.NotSupported(
fldPath.Child("policy"),
cfg.Policy,
[]string{string(PolicyAllowRefAndRefs), string(PolicyForbidRefAndRefs)},
))
}

return errs
}
Loading