-
Notifications
You must be signed in to change notification settings - Fork 468
Add config-driven attr-policy linting #1479
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
Open
alexeagle
wants to merge
16
commits into
bazelbuild:main
Choose a base branch
from
alexeagle:docs/attr-policy-test-tuning-design
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 15 commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
f372b87
Add design doc for attribute-policy linting and test-tuning
alexeagle 0b440b1
Drop per-target flaky attempts and parallelism from design
alexeagle d4b8142
Fix allow-list pattern semantics; add suppression & enforcement
alexeagle 18296c7
Make timeout bucket->seconds a configured input in testpolicy
alexeagle 38629cb
Reference bazelbuild/bazel#30108 for per-target retry-count limit
alexeagle b22d812
Treat lowering timeouts as a normal recommendation
alexeagle a3bbea8
Scope testpolicy to emitting buildozer commands only
alexeagle 7f978ff
Document boolean and dict attribute constraints in attr-policy schema.
alexeagle 1cdb5f6
Expand testpolicy design for shard_count and integer flaky.
alexeagle b54ae9d
Add config-driven attr-policy linting and buildifier JSON schema.
alexeagle abe64c8
Fix CI: sort BUILD srcs and update integration test golden.
alexeagle 4979743
Address Gemini review feedback on attr-policy design and lint.
alexeagle df97879
chore(bazel): buildifier
alexeagle ea52fdd
Enable attr-policy by default with Bazel shard_count constraint.
alexeagle b13398b
Add default attr-policy checks for deprecated license attributes.
alexeagle a39b779
Add allowListItems list constraint to attr-policy.
alexeagle File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,227 @@ | ||
| package config | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "path" | ||
| "strings" | ||
|
|
||
| "github.com/bazelbuild/buildtools/labels" | ||
| "github.com/bazelbuild/buildtools/warn" | ||
| ) | ||
|
|
||
| // AttrPolicy is the attrPolicy block in .buildifier.json. | ||
| type AttrPolicy struct { | ||
| Rules []AttrPolicyRule `json:"rules,omitempty"` | ||
| } | ||
|
|
||
| // AttrPolicyRule is a single attribute policy rule. | ||
| type AttrPolicyRule struct { | ||
| Name string `json:"name"` | ||
| RuleKinds []string `json:"ruleKinds,omitempty"` | ||
| Attr string `json:"attr"` | ||
| ForbidValues []string `json:"forbidValues,omitempty"` | ||
| RequireValues []string `json:"requireValues,omitempty"` | ||
| ForbidListItems []string `json:"forbidListItems,omitempty"` | ||
| RequireListItems []string `json:"requireListItems,omitempty"` | ||
| ForbidDictEntries map[string]string `json:"forbidDictEntries,omitempty"` | ||
| RequireDictEntries map[string]string `json:"requireDictEntries,omitempty"` | ||
| ForbidDictKeys []string `json:"forbidDictKeys,omitempty"` | ||
| MinValue *int `json:"minValue,omitempty"` | ||
| MaxValue *int `json:"maxValue,omitempty"` | ||
| Required bool `json:"required,omitempty"` | ||
| ForbidPresence bool `json:"forbidPresence,omitempty"` | ||
| Allowlist []string `json:"allowlist,omitempty"` | ||
| Suppressible *bool `json:"suppressible,omitempty"` | ||
| Message string `json:"message,omitempty"` | ||
| } | ||
|
|
||
| func compileAttrPolicy(policy *AttrPolicy) ([]warn.AttrPolicyRuleCompiled, error) { | ||
| if policy == nil || len(policy.Rules) == 0 { | ||
| return warn.DefaultAttrPolicyRules(), nil | ||
| } | ||
| seen := make(map[string]bool) | ||
| var compiled []warn.AttrPolicyRuleCompiled | ||
| for i := range policy.Rules { | ||
| rule := &policy.Rules[i] | ||
| c, err := compileAttrPolicyRule(rule, seen) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("attrPolicy.rules[%d]: %w", i, err) | ||
| } | ||
| compiled = append(compiled, c) | ||
| } | ||
| return compiled, nil | ||
| } | ||
|
|
||
| func compileAttrPolicyRule(rule *AttrPolicyRule, seen map[string]bool) (warn.AttrPolicyRuleCompiled, error) { | ||
| name := strings.TrimSpace(rule.Name) | ||
| if name == "" { | ||
| return warn.AttrPolicyRuleCompiled{}, fmt.Errorf("attrPolicy rule %q: name is required", rule.Name) | ||
| } | ||
| if seen[name] { | ||
| return warn.AttrPolicyRuleCompiled{}, fmt.Errorf("attrPolicy rule %q: duplicate name", name) | ||
| } | ||
| seen[name] = true | ||
|
|
||
| attr := strings.TrimSpace(rule.Attr) | ||
| if attr == "" { | ||
| return warn.AttrPolicyRuleCompiled{}, fmt.Errorf("attrPolicy rule %q: attr is required", name) | ||
| } | ||
|
|
||
| family, families, err := attrPolicyConstraintFamily(rule) | ||
| if err != nil { | ||
| return warn.AttrPolicyRuleCompiled{}, fmt.Errorf("attrPolicy rule %q: %w", name, err) | ||
| } | ||
| if rule.ForbidPresence && rule.Required { | ||
| return warn.AttrPolicyRuleCompiled{}, fmt.Errorf("attrPolicy rule %q: forbidPresence and required cannot both be true", name) | ||
| } | ||
| if families == 0 && !rule.Required { | ||
| return warn.AttrPolicyRuleCompiled{}, fmt.Errorf("attrPolicy rule %q: at least one constraint, required=true, or forbidPresence=true is required", name) | ||
| } | ||
|
|
||
| for _, kindGlob := range rule.RuleKinds { | ||
| if _, err := path.Match(kindGlob, "x"); err != nil { | ||
| return warn.AttrPolicyRuleCompiled{}, fmt.Errorf("attrPolicy rule %q: malformed ruleKinds glob %q: %w", name, kindGlob, err) | ||
| } | ||
| } | ||
|
|
||
| allowlist, err := compileAllowlistPatterns(name, rule.Allowlist) | ||
| if err != nil { | ||
| return warn.AttrPolicyRuleCompiled{}, err | ||
| } | ||
|
|
||
| suppressible := true | ||
| if rule.Suppressible != nil { | ||
| suppressible = *rule.Suppressible | ||
| } | ||
|
|
||
| return warn.AttrPolicyRuleCompiled{ | ||
| Name: name, | ||
| RuleKinds: append([]string(nil), rule.RuleKinds...), | ||
| Attr: attr, | ||
| Family: family, | ||
| ForbidValues: append([]string(nil), rule.ForbidValues...), | ||
| RequireValues: append([]string(nil), rule.RequireValues...), | ||
| ForbidListItems: append([]string(nil), rule.ForbidListItems...), | ||
| RequireListItems: append([]string(nil), rule.RequireListItems...), | ||
| ForbidDictEntries: copyStringMap(rule.ForbidDictEntries), | ||
| RequireDictEntries: copyStringMap(rule.RequireDictEntries), | ||
| ForbidDictKeys: append([]string(nil), rule.ForbidDictKeys...), | ||
| MinValue: cloneIntPtr(rule.MinValue), | ||
| MaxValue: cloneIntPtr(rule.MaxValue), | ||
| Required: rule.Required, | ||
| Allowlist: allowlist, | ||
| Suppressible: suppressible, | ||
| Message: rule.Message, | ||
| }, nil | ||
| } | ||
|
|
||
| func attrPolicyConstraintFamily(rule *AttrPolicyRule) (warn.AttrPolicyConstraintFamily, int, error) { | ||
| scalar := len(rule.ForbidValues) > 0 || len(rule.RequireValues) > 0 | ||
| list := len(rule.ForbidListItems) > 0 || len(rule.RequireListItems) > 0 | ||
| dict := len(rule.ForbidDictEntries) > 0 || len(rule.RequireDictEntries) > 0 || len(rule.ForbidDictKeys) > 0 | ||
| numeric := rule.MinValue != nil || rule.MaxValue != nil | ||
| presence := rule.ForbidPresence | ||
|
|
||
| families := 0 | ||
| var family warn.AttrPolicyConstraintFamily | ||
| if scalar { | ||
| families++ | ||
| family = warn.AttrPolicyScalarFamily | ||
| } | ||
| if list { | ||
| families++ | ||
| family = warn.AttrPolicyListFamily | ||
| } | ||
| if dict { | ||
| families++ | ||
| family = warn.AttrPolicyDictFamily | ||
| } | ||
| if numeric { | ||
| families++ | ||
| family = warn.AttrPolicyNumericFamily | ||
| } | ||
| if presence { | ||
| families++ | ||
| family = warn.AttrPolicyForbidPresenceFamily | ||
| } | ||
| if families > 1 { | ||
| return 0, families, fmt.Errorf("cannot mix scalar, list, dict, numeric, and presence constraint families") | ||
| } | ||
| if numeric && rule.MinValue != nil && rule.MaxValue != nil && *rule.MinValue > *rule.MaxValue { | ||
| return 0, families, fmt.Errorf("minValue must be <= maxValue") | ||
| } | ||
| return family, families, nil | ||
| } | ||
|
|
||
| func compileAllowlistPatterns(ruleName string, entries []string) ([]warn.AttrPolicyAllowlistPattern, error) { | ||
| var patterns []warn.AttrPolicyAllowlistPattern | ||
| for _, entry := range entries { | ||
| p, err := parseAllowlistPattern(entry) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("attrPolicy rule %q: allowlist entry %q: %w", ruleName, entry, err) | ||
| } | ||
| patterns = append(patterns, p) | ||
| } | ||
| return patterns, nil | ||
| } | ||
|
|
||
| func parseAllowlistPattern(entry string) (warn.AttrPolicyAllowlistPattern, error) { | ||
| if entry == "" { | ||
| return warn.AttrPolicyAllowlistPattern{}, fmt.Errorf("empty pattern") | ||
| } | ||
| if strings.HasPrefix(entry, "@") { | ||
| return warn.AttrPolicyAllowlistPattern{}, fmt.Errorf("repository-qualified entries are not supported") | ||
| } | ||
| if entry == "..." { | ||
| return warn.AttrPolicyAllowlistPattern{}, fmt.Errorf("bare ... is not supported") | ||
| } | ||
| if entry == "//..." { | ||
| return warn.AttrPolicyAllowlistPattern{Kind: warn.AttrPolicyAllowAll}, nil | ||
| } | ||
| if !strings.HasPrefix(entry, "//") { | ||
| return warn.AttrPolicyAllowlistPattern{}, fmt.Errorf("pattern must start with //") | ||
| } | ||
| if strings.HasSuffix(entry, "/...") { | ||
| pkg := strings.TrimPrefix(entry, "//") | ||
| pkg = strings.TrimSuffix(pkg, "/...") | ||
| if pkg == "" { | ||
| return warn.AttrPolicyAllowlistPattern{}, fmt.Errorf("invalid recursive pattern") | ||
| } | ||
| if strings.Contains(pkg, ":") { | ||
| return warn.AttrPolicyAllowlistPattern{}, fmt.Errorf("invalid recursive pattern") | ||
| } | ||
| return warn.AttrPolicyAllowlistPattern{Kind: warn.AttrPolicyAllowRecursive, Pkg: pkg}, nil | ||
| } | ||
|
|
||
| label := labels.Parse(entry) | ||
| if label.Target == "all" || label.Target == "*" { | ||
| if label.Package == "" { | ||
| return warn.AttrPolicyAllowlistPattern{}, fmt.Errorf("invalid package pattern") | ||
| } | ||
| return warn.AttrPolicyAllowlistPattern{Kind: warn.AttrPolicyAllowPackageAll, Pkg: label.Package}, nil | ||
| } | ||
| return warn.AttrPolicyAllowlistPattern{ | ||
| Kind: warn.AttrPolicyAllowExact, | ||
| Pkg: label.Package, | ||
| Target: label.Target, | ||
| }, nil | ||
| } | ||
|
|
||
| func copyStringMap(in map[string]string) map[string]string { | ||
| if len(in) == 0 { | ||
| return nil | ||
| } | ||
| out := make(map[string]string, len(in)) | ||
| for k, v := range in { | ||
| out[k] = v | ||
| } | ||
| return out | ||
| } | ||
|
|
||
| func cloneIntPtr(v *int) *int { | ||
| if v == nil { | ||
| return nil | ||
| } | ||
| c := *v | ||
| return &c | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
(moving the discussion from the private messaging to Github for visibility)
Should we provide a way to let people define their own rules and use the default at the same time, without copying and maintaining the list of default rules? My suggestion was to allow something like
would it be a common usecase that a repo maintainer wants to define something custom for their repo without disabling the default checks?
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.
I would guess the majority of corporate monorepo users benefit from creating some enforcement of their local conventions (for example, a lexicon of allowed
tagsvalues so they don't proliferate meaninglessly)Personally, I'd like to keep the full list in one place, so I would just copy and modify the default settings. But I'm happy to follow your guidance.
Some complications with
keepDefault:what happens if a user does
does the forbidValues compose, or is it overridden? How would a user say that no timeout values should be forbidden? What about "allowlist"? Can users provide a name that collides with a default?