-
Notifications
You must be signed in to change notification settings - Fork 21
Add references linter #163
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 4 commits
4e54c0c
4807f4b
7f06ecd
2502e96
c5a8a96
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -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'. | ||
|
|
||
| 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`. | ||
|
||
| ``` | ||
|
|
||
| **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' | ||
|
||
| - **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 | ||
|
||
|
|
||
| ## SSATags | ||
|
|
||
| The `ssatags` linter ensures that array fields in Kubernetes API objects have the appropriate | ||
|
|
||
| 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 { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?)", | ||
|
||
| 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 { | ||
|
||
| conventions = append(conventions, | ||
|
||
| namingconventions.Convention{ | ||
| Name: "forbid-refs", | ||
| ViolationMatcher: "(?i)refs([^a-z]|$)", | ||
| Operation: namingconventions.OperationInform, | ||
|
||
| Message: "should not use 'Refs'", | ||
| }, | ||
| namingconventions.Convention{ | ||
| Name: "forbid-ref", | ||
| ViolationMatcher: "(?i)ref([^ers]|$)", | ||
| Operation: namingconventions.OperationInform, | ||
| Message: "should not use 'Ref'", | ||
| }, | ||
| ) | ||
| } | ||
|
|
||
| return conventions | ||
| } | ||
| 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") | ||
| } |
| 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"` | ||
| } |
| 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 | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. NIt, probably best to remove the blank lines in these code blocks There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| 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 | ||
| } |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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,
noreferenceornoreferencesThere was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have
nophaseandnotimestampwhich are naming conventions