Skip to content

Conversation

@yongruilin
Copy link
Contributor

This PR introduces a new dependenttags linter to enforce dependencies between markers, preventing API inconsistencies where one marker requires another.

The linter is configurable with rules and supports requiring either all (default) or any of the specified dependent markers.

Fixes #146

@k8s-ci-robot
Copy link
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: yongruilin
Once this PR has been reviewed and has the lgtm label, please assign jpbetz for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@k8s-ci-robot k8s-ci-robot added cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. size/L Denotes a PR that changes 100-499 lines, ignoring generated files. labels Oct 28, 2025
docs/linters.md Outdated
Comment on lines 111 to 113
- main: "listType"
dependents:
- "k8s:listType"
Copy link
Contributor

Choose a reason for hiding this comment

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

When there are dependencies, how do values work? Can this linter fix dependencies? If I want the same value between these two, is that achievable? If I wanted different values, is that achievable?

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. Values are ignored: The linter only checks if a marker like +listType exists; it does not care about its value (e.g., =map).
  2. No automatic fixes: The linter will report that a dependent marker is missing, but it cannot automatically add it for you.
  3. Value matching is not supported: Enforcing that two markers have the same or different values is outside the scope of this linter.

I have updated the docs/linters.md file with a "Behavior" section to make this clear.

Comment on lines 46 to 49
// defaultConfig is the default configuration for the dependenttags linter.
var defaultConfig = Config{
Rules: []Rule{},
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Since this is an empty config, do we really need it?

}

rulesPath := fldPath.Child("rules")
for i, rule := range cfg.Rules {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should be an error to have no rules

@k8s-ci-robot k8s-ci-robot added the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Oct 29, 2025
@k8s-ci-robot k8s-ci-robot added size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. and removed size/L Denotes a PR that changes 100-499 lines, ignoring generated files. labels Oct 29, 2025
@yongruilin yongruilin force-pushed the dependent-tag branch 2 times, most recently from 547d0ff to c1dcbd1 Compare October 29, 2025 21:15
@k8s-ci-robot k8s-ci-robot removed the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Oct 29, 2025
Introduces a new configurable linter named `dependenttags` to enforce dependencies between markers. This linter ensures that if a main marker is present on a field, a set of dependent markers are also present, preventing API inconsistencies.

The linter supports two dependency types:
- `All`: Requires all dependent markers to be present.
- `Any`: Requires at least one of the dependent markers to be present.

This addresses issue kubernetes-sigs#146.
Adds documentation for the new `dependenttags` linter to `docs/linters.md`. This includes a description of the linter, its configuration options, and examples of how to use it.
Copy link
Contributor

@JoelSpeed JoelSpeed left a comment

Choose a reason for hiding this comment

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

Please ensure we consistently updated all/any to All/Any

I think it would be useful (especially for the listType case) to be able to say "this identifier with this value relies on this identifier"

What if we changed the config to

- identifier: ...
  payload: ... # optional
  dependents:
  - identifier: ...
    payload: ... # optional

And then you would be able to say that if it has a specified payload, then the dependents are either requiring just the identifier, or the identifier with a particular payload

This could become a bit more complex with DV parsing though, not sure how arguments will fit into this CC @everettraven


This linter only checks for the presence or absence of markers; it does not inspect or enforce specific values within those markers. Therefore:

- **Values:** The linter does not care about the values of the `identifier` or `dependent` markers. It only verifies if the markers themselves are present.
Copy link
Contributor

Choose a reason for hiding this comment

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

+listType=map needs +listMapKey=..., that cannot be represented here then? Feels like that could be something that is useful

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It can, you can directly put identifier as +listType=map

Copy link
Contributor

Choose a reason for hiding this comment

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

Please make sure there are test cases for this


type Union struct {
// +k8s:unionMember
InvalidMember string // want "field with marker \\+k8s:unionMember is missing required marker\\(s\\): \\+k8s:optional"
Copy link
Contributor

Choose a reason for hiding this comment

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

We should include the field name, and ideally struct name in the error message

Suggested change
InvalidMember string // want "field with marker \\+k8s:unionMember is missing required marker\\(s\\): \\+k8s:optional"
InvalidMember string // want "field Union.InvalidMember with marker \\+k8s:unionMember is missing required marker\\(s\\): \\+k8s:optional"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated.


const (
// DependencyTypeAll indicates that all dependent markers are required.
DependencyTypeAll DependencyType = "all"
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
DependencyTypeAll DependencyType = "all"
DependencyTypeAll DependencyType = "All"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated.

Comment on lines 104 to 122
t.Run(tc.name, func(t *testing.T) {
initializer := dependenttags.Initializer()
errs := initializer.ValidateConfig(tc.cfg, field.NewPath(""))

if tc.expectErr && len(errs) == 0 {
t.Errorf("expected validation errors, but got none")
}

if !tc.expectErr && len(errs) > 0 {
t.Errorf("unexpected validation errors: %v", errs)
}

if !tc.expectErr {
if _, err := initializer.Init(tc.cfg); err != nil {
t.Errorf("unexpected error initializing analyzer: %v", err)
}
}
})
}
Copy link
Contributor

Choose a reason for hiding this comment

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

The rest of our tests use gomega for assertions, worth doing the same 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.

Updated.

}

if rule.Type == "" {
errs = append(errs, field.Required(rulesPath.Index(i).Child("type"), "type must be explicitly set to 'all' or 'any'"))
Copy link
Contributor

Choose a reason for hiding this comment

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

Use a format string with the actual constant values please

case DependencyTypeAll, DependencyTypeAny:
// valid
default:
errs = append(errs, field.Invalid(rulesPath.Index(i).Child("type"), rule.Type, "type must be 'all' or 'any'"))
Copy link
Contributor

Choose a reason for hiding this comment

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

Doesn't invalid take a list of expected valid values?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It seems not. I've updated to use format string in error detail.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. size/XL Denotes a PR that changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

New Linter: dependenttags

4 participants